feat: vendor gitea 1.16.2
This commit is contained in:
@@ -1,92 +0,0 @@
|
||||
# End to end tests
|
||||
|
||||
E2e tests largely follow the same syntax as [integration tests](../integration).
|
||||
Whereas integration tests are intended to mock and stress the back-end, server-side code, e2e tests the interface between front-end and back-end, as well as visual regressions with both assertions and visual comparisons.
|
||||
They can be run with make commands for the appropriate backends, namely:
|
||||
```shell
|
||||
make test-sqlite
|
||||
make test-pgsql
|
||||
make test-mysql
|
||||
make test-mssql
|
||||
```
|
||||
|
||||
Make sure to perform a clean front-end build before running tests:
|
||||
```
|
||||
make clean frontend
|
||||
```
|
||||
|
||||
## Install playwright system dependencies
|
||||
```
|
||||
npx playwright install-deps
|
||||
```
|
||||
|
||||
|
||||
## Run all tests via local act_runner
|
||||
```
|
||||
act_runner exec -W ./.github/workflows/pull-e2e-tests.yml --event=pull_request --default-actions-url="https://github.com" -i catthehacker/ubuntu:runner-latest
|
||||
```
|
||||
|
||||
## Run sqlite e2e tests
|
||||
Start tests
|
||||
```
|
||||
make test-e2e-sqlite
|
||||
```
|
||||
|
||||
## Run MySQL e2e tests
|
||||
Setup a MySQL database inside docker
|
||||
```
|
||||
docker run -e "MYSQL_DATABASE=test" -e "MYSQL_ALLOW_EMPTY_PASSWORD=yes" -p 3306:3306 --rm --name mysql mysql:latest #(just ctrl-c to stop db and clean the container)
|
||||
docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" --rm --name elasticsearch elasticsearch:7.6.0 #(in a second terminal, just ctrl-c to stop db and clean the container)
|
||||
```
|
||||
Start tests based on the database container
|
||||
```
|
||||
TEST_MYSQL_HOST=localhost:3306 TEST_MYSQL_DBNAME=test TEST_MYSQL_USERNAME=root TEST_MYSQL_PASSWORD='' make test-e2e-mysql
|
||||
```
|
||||
|
||||
## Run pgsql e2e tests
|
||||
Setup a pgsql database inside docker
|
||||
```
|
||||
docker run -e "POSTGRES_DB=test" -p 5432:5432 --rm --name pgsql postgres:latest #(just ctrl-c to stop db and clean the container)
|
||||
```
|
||||
Start tests based on the database container
|
||||
```
|
||||
TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgres TEST_PGSQL_PASSWORD=postgres make test-e2e-pgsql
|
||||
```
|
||||
|
||||
## Run mssql e2e tests
|
||||
Setup a mssql database inside docker
|
||||
```
|
||||
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_PID=Standard" -e "SA_PASSWORD=MwantsaSecurePassword1" -p 1433:1433 --rm --name mssql microsoft/mssql-server-linux:latest #(just ctrl-c to stop db and clean the container)
|
||||
```
|
||||
Start tests based on the database container
|
||||
```
|
||||
TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=gitea_test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql
|
||||
```
|
||||
|
||||
## Running individual tests
|
||||
|
||||
Example command to run `example.test.e2e.ts` test file:
|
||||
|
||||
_Note: unlike integration tests, this filtering is at the file level, not function_
|
||||
|
||||
For SQLite:
|
||||
|
||||
```
|
||||
make test-e2e-sqlite#example
|
||||
```
|
||||
|
||||
For other databases(replace `mssql` to `mysql` or `pgsql`):
|
||||
|
||||
```
|
||||
TEST_MSSQL_HOST=localhost:1433 TEST_MSSQL_DBNAME=test TEST_MSSQL_USERNAME=sa TEST_MSSQL_PASSWORD=MwantsaSecurePassword1 make test-e2e-mssql#example
|
||||
```
|
||||
|
||||
## Visual testing
|
||||
|
||||
Although the main goal of e2e is assertion testing, we have added a framework for visual regress testing. If you are working on front-end features, please use the following:
|
||||
- Check out `main`, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1` to generate outputs. This will initially fail, as no screenshots exist. You can run the e2e tests again to assert it passes.
|
||||
- Check out your branch, `make clean frontend`, and run e2e tests with `VISUAL_TEST=1`. You should be able to assert you front-end changes don't break any other tests unintentionally.
|
||||
|
||||
VISUAL_TEST=1 will create screenshots in tests/e2e/test-snapshots. The test will fail the first time this is enabled (until we get visual test image persistence figured out), because it will be testing against an empty screenshot folder.
|
||||
|
||||
ACCEPT_VISUAL=1 will overwrite the snapshot images with new images.
|
||||
@@ -0,0 +1,20 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
|
||||
|
||||
test('codeeditor textarea updates correctly', async ({page, request}) => {
|
||||
const repoName = `e2e-codeeditor-${randomString(8)}`;
|
||||
await Promise.all([apiCreateRepo(request, {name: repoName}), login(page)]);
|
||||
try {
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/_new/main`);
|
||||
await page.getByPlaceholder('Name your file…').fill('test.js');
|
||||
await expect(page.locator('.editor-loading')).toBeHidden();
|
||||
const editor = page.locator('.cm-content[role="textbox"]');
|
||||
await expect(editor).toBeVisible();
|
||||
await editor.click();
|
||||
await page.keyboard.type('const hello = "world";');
|
||||
await expect(page.locator('textarea[name="content"]')).toHaveValue('const hello = "world";');
|
||||
} finally {
|
||||
await apiDeleteRepo(request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
}
|
||||
});
|
||||
@@ -1,115 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// This is primarily coped from /tests/integration/integration_test.go
|
||||
// TODO: Move common functions to shared file
|
||||
|
||||
//nolint:forbidigo // use of print functions is allowed in tests
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/testlogger"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers"
|
||||
"code.gitea.io/gitea/tests"
|
||||
)
|
||||
|
||||
var testE2eWebRoutes *web.Router
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
defer log.GetManager().Close()
|
||||
|
||||
managerCtx, cancel := context.WithCancel(context.Background())
|
||||
graceful.InitManager(managerCtx)
|
||||
defer cancel()
|
||||
|
||||
tests.InitTest(false)
|
||||
testE2eWebRoutes = routers.NormalRoutes()
|
||||
|
||||
err := unittest.InitFixtures(
|
||||
unittest.FixturesOptions{
|
||||
Dir: filepath.Join(filepath.Dir(setting.AppPath), "models/fixtures/"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Printf("Error initializing test database: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
exitVal := m.Run()
|
||||
|
||||
testlogger.WriterCloser.Reset()
|
||||
|
||||
if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
|
||||
fmt.Printf("util.RemoveAll: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
|
||||
fmt.Printf("Unable to remove repo indexer: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(exitVal)
|
||||
}
|
||||
|
||||
// TestE2e should be the only test e2e necessary. It will collect all "*.test.e2e.ts" files in this directory and build a test for each.
|
||||
func TestE2e(t *testing.T) {
|
||||
// Find the paths of all e2e test files in test directory.
|
||||
searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.ts")
|
||||
paths, err := filepath.Glob(searchGlob)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(paths) == 0 {
|
||||
t.Fatal(fmt.Errorf("No e2e tests found in %s", searchGlob))
|
||||
}
|
||||
|
||||
runArgs := []string{"npx", "playwright", "test"}
|
||||
|
||||
// To update snapshot outputs
|
||||
if _, set := os.LookupEnv("ACCEPT_VISUAL"); set {
|
||||
runArgs = append(runArgs, "--update-snapshots")
|
||||
}
|
||||
|
||||
// Create new test for each input file
|
||||
for _, path := range paths {
|
||||
_, filename := filepath.Split(path)
|
||||
testname := filename[:len(filename)-len(filepath.Ext(path))]
|
||||
|
||||
t.Run(testname, func(t *testing.T) {
|
||||
// Default 2 minute timeout
|
||||
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||
cmd := exec.Command(runArgs[0], runArgs...)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "GITEA_URL="+setting.AppURL)
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
// Currently colored output is conflicting. Using Printf until that is resolved.
|
||||
fmt.Printf("%v", stdout.String())
|
||||
fmt.Printf("%v", stderr.String())
|
||||
log.Fatal("Playwright Failed: %s", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%v", stdout.String())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
GITEA_TEST_E2E_DOMAIN: string;
|
||||
GITEA_TEST_E2E_USER: string;
|
||||
GITEA_TEST_E2E_EMAIL: string;
|
||||
GITEA_TEST_E2E_PASSWORD: string;
|
||||
GITEA_TEST_E2E_URL: string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {loginUser, baseUrl, apiUserHeaders, apiCreateUser, apiDeleteUser, apiCreateRepo, apiCreateIssue, apiStartStopwatch, timeoutFactor, randomString} from './utils.ts';
|
||||
|
||||
// These tests rely on a short EVENT_SOURCE_UPDATE_TIME in the e2e server config.
|
||||
test.describe('events', () => {
|
||||
test('notification count', async ({page, request}) => {
|
||||
const owner = `ev-notif-owner-${randomString(8)}`;
|
||||
const commenter = `ev-notif-commenter-${randomString(8)}`;
|
||||
const repoName = `ev-notif-${randomString(8)}`;
|
||||
|
||||
await Promise.all([apiCreateUser(request, owner), apiCreateUser(request, commenter)]);
|
||||
|
||||
// Create repo and login in parallel — repo is needed for the issue, login for the event stream
|
||||
await Promise.all([
|
||||
apiCreateRepo(request, {name: repoName, headers: apiUserHeaders(owner)}),
|
||||
loginUser(page, owner),
|
||||
]);
|
||||
const badge = page.locator('a.not-mobile .notification_count');
|
||||
await expect(badge).toBeHidden();
|
||||
|
||||
// Create issue as another user — this generates a notification delivered via server push
|
||||
await apiCreateIssue(request, owner, repoName, {title: 'events notification test', headers: apiUserHeaders(commenter)});
|
||||
|
||||
// Wait for the notification badge to appear via server event
|
||||
await expect(badge).toBeVisible({timeout: 15000 * timeoutFactor});
|
||||
|
||||
// Cleanup
|
||||
await Promise.all([apiDeleteUser(request, commenter), apiDeleteUser(request, owner)]);
|
||||
});
|
||||
|
||||
test('stopwatch', async ({page, request}) => {
|
||||
const name = `ev-sw-${randomString(8)}`;
|
||||
const headers = apiUserHeaders(name);
|
||||
|
||||
await apiCreateUser(request, name);
|
||||
|
||||
// Create repo, issue, and start stopwatch before login
|
||||
await apiCreateRepo(request, {name, headers});
|
||||
await apiCreateIssue(request, name, name, {title: 'events stopwatch test', headers});
|
||||
await apiStartStopwatch(request, name, name, 1, {headers});
|
||||
|
||||
// Login — page renders with the active stopwatch element
|
||||
await loginUser(page, name);
|
||||
|
||||
// Verify stopwatch is visible and links to the correct issue
|
||||
const stopwatch = page.locator('.active-stopwatch.not-mobile');
|
||||
await expect(stopwatch).toBeVisible();
|
||||
|
||||
// Cleanup
|
||||
await apiDeleteUser(request, name);
|
||||
});
|
||||
|
||||
test('logout propagation', async ({browser, request}) => {
|
||||
const name = `ev-logout-${randomString(8)}`;
|
||||
|
||||
await apiCreateUser(request, name);
|
||||
|
||||
// Use a single context so both pages share the same session and SharedWorker
|
||||
const context = await browser.newContext({baseURL: baseUrl()});
|
||||
const page1 = await context.newPage();
|
||||
const page2 = await context.newPage();
|
||||
|
||||
await loginUser(page1, name);
|
||||
|
||||
// Navigate page2 so it connects to the shared event stream
|
||||
await page2.goto('/');
|
||||
|
||||
// Verify page2 is logged in
|
||||
await expect(page2.getByRole('link', {name: 'Sign In'})).toBeHidden();
|
||||
|
||||
// Give page2's SharedWorker time to register its SSE connection on the
|
||||
// server — otherwise the logout event can race the connection and be
|
||||
// silently dropped. See https://github.com/go-gitea/gitea/pull/37403
|
||||
// In the future, we can set an attribute to HTML page when the connection is established,
|
||||
// then here we can just wait for that attribute (it should also work for the planned WebSocket SharedWorker)
|
||||
await page2.waitForTimeout(500); // eslint-disable-line playwright/no-wait-for-timeout
|
||||
|
||||
// Logout from page1 — this sends a logout event to all tabs
|
||||
await page1.goto('/user/logout');
|
||||
|
||||
// page2 should be redirected via the logout event
|
||||
await expect(page2.getByRole('link', {name: 'Sign In'})).toBeVisible();
|
||||
|
||||
await context.close();
|
||||
|
||||
// Cleanup
|
||||
await apiDeleteUser(request, name);
|
||||
});
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login_user, save_visual, load_logged_in_context} from './utils_e2e.ts';
|
||||
|
||||
test.beforeAll(async ({browser}, workerInfo) => {
|
||||
await login_user(browser, workerInfo, 'user2');
|
||||
});
|
||||
|
||||
test('homepage', async ({page}) => {
|
||||
const response = await page.goto('/');
|
||||
expect(response?.status()).toBe(200); // Status OK
|
||||
await expect(page).toHaveTitle(/^Gitea: Git with a cup of tea\s*$/);
|
||||
await expect(page.locator('.logo')).toHaveAttribute('src', '/assets/img/logo.svg');
|
||||
});
|
||||
|
||||
test('register', async ({page}, workerInfo) => {
|
||||
const response = await page.goto('/user/sign_up');
|
||||
expect(response?.status()).toBe(200); // Status OK
|
||||
await page.locator('input[name=user_name]').fill(`e2e-test-${workerInfo.workerIndex}`);
|
||||
await page.locator('input[name=email]').fill(`e2e-test-${workerInfo.workerIndex}@test.com`);
|
||||
await page.locator('input[name=password]').fill('test123test123');
|
||||
await page.locator('input[name=retype]').fill('test123test123');
|
||||
await page.click('form button.ui.primary.button:visible');
|
||||
// Make sure we routed to the home page. Else login failed.
|
||||
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
|
||||
await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible();
|
||||
await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!');
|
||||
|
||||
save_visual(page);
|
||||
});
|
||||
|
||||
test('login', async ({page}, workerInfo) => {
|
||||
const response = await page.goto('/user/login');
|
||||
expect(response?.status()).toBe(200); // Status OK
|
||||
|
||||
await page.locator('input[name=user_name]').fill(`user2`);
|
||||
await page.locator('input[name=password]').fill(`password`);
|
||||
await page.click('form button.ui.primary.button:visible');
|
||||
|
||||
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
|
||||
|
||||
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
|
||||
|
||||
save_visual(page);
|
||||
});
|
||||
|
||||
test('logged in user', async ({browser}, workerInfo) => {
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Make sure we routed to the home page. Else login failed.
|
||||
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
|
||||
|
||||
save_visual(page);
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
|
||||
test('explore repositories', async ({page}) => {
|
||||
await page.goto('/explore/repos');
|
||||
await expect(page.getByPlaceholder('Search repos…')).toBeVisible();
|
||||
await expect(page.getByRole('link', {name: 'Repositories'})).toBeVisible();
|
||||
});
|
||||
|
||||
test('explore users', async ({page}) => {
|
||||
await page.goto('/explore/users');
|
||||
await expect(page.getByPlaceholder('Search users…')).toBeVisible();
|
||||
});
|
||||
|
||||
test('explore organizations', async ({page}) => {
|
||||
await page.goto('/explore/organizations');
|
||||
await expect(page.getByPlaceholder('Search orgs…')).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateFile, apiDeleteRepo, assertFlushWithParent, assertNoJsError, randomString} from './utils.ts';
|
||||
|
||||
test('external file', async ({page, request}) => {
|
||||
const repoName = `e2e-external-render-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await Promise.all([
|
||||
apiCreateRepo(request, {name: repoName}),
|
||||
login(page),
|
||||
]);
|
||||
try {
|
||||
await apiCreateFile(request, owner, repoName, 'test.external', '<p>rendered content</p>');
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.external`);
|
||||
const iframe = page.locator('iframe.external-render-iframe');
|
||||
await expect(iframe).toBeVisible();
|
||||
await expect(iframe).toHaveAttribute('data-src', new RegExp(`/${owner}/${repoName}/render/branch/main/test\\.external`));
|
||||
const frame = page.frameLocator('iframe.external-render-iframe');
|
||||
await expect(frame.locator('p')).toContainText('rendered content');
|
||||
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
||||
await assertNoJsError(page);
|
||||
} finally {
|
||||
await apiDeleteRepo(request, owner, repoName);
|
||||
}
|
||||
});
|
||||
|
||||
test('openapi file', async ({page, request}) => {
|
||||
const repoName = `e2e-openapi-render-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await Promise.all([
|
||||
apiCreateRepo(request, {name: repoName}),
|
||||
login(page),
|
||||
]);
|
||||
try {
|
||||
const title = 'Test <API> & "quoted"';
|
||||
const spec = JSON.stringify({
|
||||
openapi: '3.0.0',
|
||||
info: {title, version: '1.0'},
|
||||
paths: {'/pets': {get: {responses: {'200': {description: 'OK', content: {'application/json': {schema: {$ref: '#/components/schemas/Pet'}}}}}}}},
|
||||
components: {schemas: {Pet: {type: 'object', properties: {children: {type: 'array', items: {$ref: '#/components/schemas/Pet'}}}}}},
|
||||
});
|
||||
await apiCreateFile(request, owner, repoName, 'openapi.json', spec);
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/openapi.json`);
|
||||
const iframe = page.locator('iframe.external-render-iframe');
|
||||
await expect(iframe).toBeVisible();
|
||||
const viewer = page.frameLocator('iframe.external-render-iframe').locator('#frontend-render-viewer');
|
||||
await expect(viewer.locator('.swagger-ui')).toBeVisible();
|
||||
await expect(viewer.locator('.info .title')).toContainText(title);
|
||||
// expanding the operation triggers swagger-ui's $ref resolver, which fetches window.location
|
||||
// (about:srcdoc since the iframe is loaded via srcdoc); failure surfaces as "Could not resolve reference"
|
||||
await viewer.locator('.opblock-tag').first().click();
|
||||
await viewer.locator('.opblock').first().click();
|
||||
await expect(viewer.getByText('Could not resolve reference')).toHaveCount(0);
|
||||
// poll: postMessage resize may not have settled yet when the visibility checks pass
|
||||
await expect.poll(async () => (await iframe.boundingBox())!.height).toBeGreaterThan(300);
|
||||
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
||||
await assertNoJsError(page);
|
||||
} finally {
|
||||
await apiDeleteRepo(request, owner, repoName);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {apiCreateBranch, apiCreateRepo, apiCreateFile, apiDeleteRepo, assertFlushWithParent, assertNoJsError, login, randomString} from './utils.ts';
|
||||
|
||||
test('3d model file', async ({page, request}) => {
|
||||
const repoName = `e2e-3d-render-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await apiCreateRepo(request, {name: repoName});
|
||||
try {
|
||||
const stl = 'solid test\nfacet normal 0 0 1\nouter loop\nvertex 0 0 0\nvertex 1 0 0\nvertex 0 1 0\nendloop\nendfacet\nendsolid test\n';
|
||||
await apiCreateFile(request, owner, repoName, 'test.stl', stl);
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.stl?display=rendered`);
|
||||
const iframe = page.locator('iframe.external-render-iframe');
|
||||
await expect(iframe).toBeVisible();
|
||||
const frame = page.frameLocator('iframe.external-render-iframe');
|
||||
const viewer = frame.locator('#frontend-render-viewer');
|
||||
await expect(viewer.locator('canvas')).toBeVisible();
|
||||
expect((await viewer.boundingBox())!.height).toBeGreaterThan(300);
|
||||
await assertFlushWithParent(iframe, page.locator('.file-view'));
|
||||
// bgcolor passed via gitea-iframe-bgcolor; 3D viewer reads it from body bgcolor — must match parent
|
||||
const [parentBg, iframeBg] = await Promise.all([
|
||||
page.evaluate(() => getComputedStyle(document.body).backgroundColor),
|
||||
frame.locator('body').evaluate((el) => getComputedStyle(el).backgroundColor),
|
||||
]);
|
||||
expect(iframeBg).toBe(parentBg);
|
||||
await assertNoJsError(page);
|
||||
} finally {
|
||||
await apiDeleteRepo(request, owner, repoName);
|
||||
}
|
||||
});
|
||||
|
||||
test('pdf file', async ({page, request}) => {
|
||||
// headless playwright cannot render PDFs (PDFObject.embed returns false), so this is a limited test
|
||||
const repoName = `e2e-pdf-render-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await apiCreateRepo(request, {name: repoName});
|
||||
try {
|
||||
await apiCreateFile(request, owner, repoName, 'test.pdf', '%PDF-1.0\n%%EOF\n');
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/main/test.pdf`);
|
||||
const container = page.locator('.file-view-render-container');
|
||||
await expect(container).toHaveAttribute('data-render-name', 'pdf-viewer');
|
||||
expect((await container.boundingBox())!.height).toBeGreaterThan(300);
|
||||
await assertFlushWithParent(container, page.locator('.file-view'));
|
||||
} finally {
|
||||
await apiDeleteRepo(request, owner, repoName);
|
||||
}
|
||||
});
|
||||
|
||||
test('asciicast file', async ({page, request}) => {
|
||||
// regression for repo_file.go's RefTypeNameSubURL double-escape: readme.cast on a non-ASCII branch
|
||||
// is rendered via view_readme.go (no metas override), exposing the bug as a broken player URL
|
||||
const repoName = `e2e-asciicast-render-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
const branch = '日本語-branch';
|
||||
const branchEnc = encodeURIComponent(branch);
|
||||
await Promise.all([apiCreateRepo(request, {name: repoName, autoInit: false}), login(page)]);
|
||||
try {
|
||||
const cast = '{"version": 2, "width": 80, "height": 24}\n[0.0, "o", "hi"]\n';
|
||||
await apiCreateFile(request, owner, repoName, 'readme.cast', cast);
|
||||
await apiCreateBranch(request, owner, repoName, branch);
|
||||
await page.goto(`/${owner}/${repoName}/src/branch/${branchEnc}`);
|
||||
const container = page.locator('.asciinema-player-container');
|
||||
await expect(container).toHaveAttribute('data-asciinema-player-src', `/${owner}/${repoName}/raw/branch/${branchEnc}/readme.cast`);
|
||||
await expect(container.locator('.ap-wrapper')).toBeVisible();
|
||||
expect((await container.boundingBox())!.height).toBeGreaterThan(300);
|
||||
} finally {
|
||||
await apiDeleteRepo(request, owner, repoName);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
|
||||
test('licenses.txt', async ({page}) => {
|
||||
const resp = await page.goto('/assets/licenses.txt');
|
||||
expect(resp?.status()).toBe(200);
|
||||
const content = await resp!.text();
|
||||
expect(content).toContain('@vue/');
|
||||
expect(content).toContain('code.gitea.io/');
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, logout} from './utils.ts';
|
||||
|
||||
test('homepage', async ({page}) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('img', {name: 'Logo'})).toHaveAttribute('src', '/assets/img/logo.svg');
|
||||
});
|
||||
|
||||
test('login and logout', async ({page}) => {
|
||||
await login(page);
|
||||
await logout(page);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {apiCreateRepo, apiHeaders, assertNoJsError, baseUrl, randomString} from './utils.ts';
|
||||
|
||||
test('mermaid diagram in issue', async ({page, request}) => {
|
||||
const repoName = `e2e-mermaid-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await apiCreateRepo(request, {name: repoName});
|
||||
const body = '```mermaid\nflowchart LR\n Alpha --> Beta\n Beta --> Gamma\n```\n';
|
||||
const response = await request.post(`${baseUrl()}/api/v1/repos/${owner}/${repoName}/issues`, {
|
||||
headers: apiHeaders(),
|
||||
data: {title: 'mermaid test', body},
|
||||
});
|
||||
expect(response.ok(), `create issue failed: ${response.status()}`).toBe(true);
|
||||
const {number} = await response.json();
|
||||
await page.goto(`/${owner}/${repoName}/issues/${number}`);
|
||||
|
||||
const svg = page.frameLocator('iframe.markup-content-iframe').locator('svg');
|
||||
await expect(svg).toContainText(/Alpha[\s\S]*Beta[\s\S]*Gamma/);
|
||||
|
||||
await assertNoJsError(page);
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
|
||||
|
||||
test('create a milestone', async ({page}) => {
|
||||
const repoName = `e2e-milestone-${randomString(8)}`;
|
||||
await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName})]);
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/milestones/new`);
|
||||
await page.getByPlaceholder('Title').fill('Test Milestone');
|
||||
await page.getByRole('button', {name: 'Create Milestone'}).click();
|
||||
await expect(page.locator('.milestone-list')).toContainText('Test Milestone');
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, apiDeleteOrg, randomString} from './utils.ts';
|
||||
|
||||
test('create an organization', async ({page}) => {
|
||||
const orgName = `e2e-org-${randomString(8)}`;
|
||||
await login(page);
|
||||
await page.goto('/org/create');
|
||||
await page.getByLabel('Organization Name').fill(orgName);
|
||||
await page.getByRole('button', {name: 'Create Organization'}).click();
|
||||
await expect(page).toHaveURL(new RegExp(`/org/${orgName}`));
|
||||
// delete via API because of issues related to form-fetch-action
|
||||
await apiDeleteOrg(page.request, orgName);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {login, apiCreateRepo, apiCreateIssue, apiDeleteRepo, randomString} from './utils.ts';
|
||||
|
||||
test('toggle issue reactions', async ({page, request}) => {
|
||||
const repoName = `e2e-reactions-${randomString(8)}`;
|
||||
const owner = env.GITEA_TEST_E2E_USER;
|
||||
await apiCreateRepo(request, {name: repoName});
|
||||
await Promise.all([
|
||||
apiCreateIssue(request, owner, repoName, {title: 'Reaction test'}),
|
||||
login(page),
|
||||
]);
|
||||
try {
|
||||
await page.goto(`/${owner}/${repoName}/issues/1`);
|
||||
|
||||
const issueComment = page.locator('.timeline-item.comment.first');
|
||||
|
||||
const reactionPicker = issueComment.locator('.select-reaction');
|
||||
await reactionPicker.click();
|
||||
await reactionPicker.getByLabel('+1').click();
|
||||
|
||||
const reactions = issueComment.getByRole('group', {name: 'Reactions'});
|
||||
await expect(reactions.getByRole('button', {name: /^\+1:/})).toContainText('1');
|
||||
|
||||
await reactions.getByRole('button', {name: /^\+1:/}).click();
|
||||
await expect(reactions.getByRole('button', {name: /^\+1:/})).toHaveCount(0);
|
||||
} finally {
|
||||
await apiDeleteRepo(request, owner, repoName);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {apiCreateRepo, apiDeleteRepo, randomString} from './utils.ts';
|
||||
|
||||
test('repo readme', async ({page}) => {
|
||||
const repoName = `e2e-readme-${randomString(8)}`;
|
||||
await apiCreateRepo(page.request, {name: repoName});
|
||||
await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}`);
|
||||
await expect(page.locator('#readme')).toContainText(repoName);
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import {env} from 'node:process';
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {login, logout, apiDeleteUser, randomString} from './utils.ts';
|
||||
|
||||
test.beforeEach(async ({page}) => {
|
||||
await page.goto('/user/sign_up');
|
||||
});
|
||||
|
||||
test('register page has form', async ({page}) => {
|
||||
await expect(page.getByLabel('Username')).toBeVisible();
|
||||
await expect(page.getByLabel('Email Address')).toBeVisible();
|
||||
await expect(page.getByLabel('Password', {exact: true})).toBeVisible();
|
||||
await expect(page.getByLabel('Confirm Password')).toBeVisible();
|
||||
await expect(page.getByRole('button', {name: 'Register Account'})).toBeVisible();
|
||||
});
|
||||
|
||||
test('register with empty fields shows error', async ({page}) => {
|
||||
// HTML5 required attribute prevents submission, so verify the fields are required
|
||||
await expect(page.locator('input[name="user_name"][required]')).toBeVisible();
|
||||
await expect(page.locator('input[name="email"][required]')).toBeVisible();
|
||||
await expect(page.locator('input[name="password"][required]')).toBeVisible();
|
||||
await expect(page.locator('input[name="retype"][required]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('register with mismatched passwords shows error', async ({page}) => {
|
||||
await page.getByLabel('Username').fill('e2e-register-mismatch');
|
||||
await page.getByLabel('Email Address').fill(`e2e-register-mismatch@${env.GITEA_TEST_E2E_DOMAIN}`);
|
||||
await page.getByLabel('Password', {exact: true}).fill('password123!');
|
||||
await page.getByLabel('Confirm Password').fill('different123!');
|
||||
await page.getByRole('button', {name: 'Register Account'}).click();
|
||||
await expect(page.locator('.ui.negative.message')).toBeVisible();
|
||||
});
|
||||
|
||||
test('register then login', async ({page}) => {
|
||||
const username = `e2e-register-${randomString(8)}`;
|
||||
const email = `${username}@${env.GITEA_TEST_E2E_DOMAIN}`;
|
||||
const password = 'password123!';
|
||||
|
||||
await page.getByLabel('Username').fill(username);
|
||||
await page.getByLabel('Email Address').fill(email);
|
||||
await page.getByLabel('Password', {exact: true}).fill(password);
|
||||
await page.getByLabel('Confirm Password').fill(password);
|
||||
await page.getByRole('button', {name: 'Register Account'}).click();
|
||||
|
||||
// After successful registration, should be redirected away from sign_up
|
||||
await expect(page).not.toHaveURL(/sign_up/);
|
||||
|
||||
// Logout then login with the newly created account
|
||||
await logout(page);
|
||||
await login(page, username, password);
|
||||
|
||||
// delete via API because of issues related to form-fetch-action
|
||||
await apiDeleteUser(page.request, username);
|
||||
});
|
||||
|
||||
test('register with existing username shows error', async ({page}) => {
|
||||
await page.getByLabel('Username').fill(env.GITEA_TEST_E2E_USER);
|
||||
await page.getByLabel('Email Address').fill(`e2e-duplicate@${env.GITEA_TEST_E2E_DOMAIN}`);
|
||||
await page.getByLabel('Password', {exact: true}).fill('password123!');
|
||||
await page.getByLabel('Confirm Password').fill('password123!');
|
||||
await page.getByRole('button', {name: 'Register Account'}).click();
|
||||
await expect(page.locator('.ui.negative.message')).toBeVisible();
|
||||
});
|
||||
|
||||
test('sign in link exists', async ({page}) => {
|
||||
const signInLink = page.getByText('Sign in now!');
|
||||
await expect(signInLink).toBeVisible();
|
||||
await signInLink.click();
|
||||
await expect(page).toHaveURL(/\/user\/login$/);
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {assertNoJsError} from './utils.ts';
|
||||
|
||||
test('relative-time renders without errors', async ({page}) => {
|
||||
await page.goto('/devtest/relative-time');
|
||||
const relativeTime = page.getByTestId('relative-time-now');
|
||||
await expect(relativeTime).toHaveAttribute('data-tooltip-content', /.+/);
|
||||
await expect(relativeTime).toHaveText('now');
|
||||
await assertNoJsError(page);
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import {env} from 'node:process';
|
||||
import {test} from '@playwright/test';
|
||||
import {login, apiDeleteRepo, randomString} from './utils.ts';
|
||||
|
||||
test('create a repository', async ({page}) => {
|
||||
const repoName = `e2e-repo-${randomString(8)}`;
|
||||
await login(page);
|
||||
await page.goto('/repo/create');
|
||||
await page.locator('input[name="repo_name"]').fill(repoName);
|
||||
await page.getByRole('button', {name: 'Create Repository'}).click();
|
||||
await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`));
|
||||
await apiDeleteRepo(page.request, env.GITEA_TEST_E2E_USER, repoName);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import {test, expect} from '@playwright/test';
|
||||
import {loginUser, apiCreateUser, apiDeleteUser, randomString} from './utils.ts';
|
||||
|
||||
test('update profile biography', async ({page, request}) => {
|
||||
const username = `e2e-settings-${randomString(8)}`;
|
||||
const bio = `e2e-bio-${randomString(8)}`;
|
||||
await apiCreateUser(request, username);
|
||||
try {
|
||||
await loginUser(page, username);
|
||||
await page.goto('/user/settings');
|
||||
await page.getByLabel('Biography').fill(bio);
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue(bio);
|
||||
await page.getByLabel('Biography').fill('');
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue('');
|
||||
} finally {
|
||||
await apiDeleteUser(request, username);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import {env} from 'node:process';
|
||||
import {expect} from '@playwright/test';
|
||||
import type {APIRequestContext, Locator, Page} from '@playwright/test';
|
||||
|
||||
/** Generate a random alphanumeric string. */
|
||||
export function randomString(length: number): string {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let index = 0; index < length; index++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const timeoutFactor = Number(env.GITEA_TEST_E2E_TIMEOUT_FACTOR) || 1;
|
||||
|
||||
export function baseUrl() {
|
||||
return env.GITEA_TEST_E2E_URL?.replace(/\/$/g, '');
|
||||
}
|
||||
|
||||
function apiAuthHeader(username: string, password: string) {
|
||||
return {Authorization: `Basic ${globalThis.btoa(`${username}:${password}`)}`};
|
||||
}
|
||||
|
||||
export function apiHeaders() {
|
||||
return apiAuthHeader(env.GITEA_TEST_E2E_USER, env.GITEA_TEST_E2E_PASSWORD);
|
||||
}
|
||||
|
||||
async function apiRetry(fn: () => Promise<{ok: () => boolean; status: () => number; text: () => Promise<string>}>, label: string) {
|
||||
const maxAttempts = 5;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const response = await fn();
|
||||
if (response.ok()) return;
|
||||
if ([500, 502, 503].includes(response.status()) && attempt < maxAttempts - 1) {
|
||||
const jitter = Math.random() * 500;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * (attempt + 1) + jitter));
|
||||
continue;
|
||||
}
|
||||
throw new Error(`${label} failed: ${response.status()} ${await response.text()}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiCreateRepo(requestContext: APIRequestContext, {name, autoInit = true, headers}: {name: string; autoInit?: boolean; headers?: Record<string, string>}) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/user/repos`, {
|
||||
headers: headers || apiHeaders(),
|
||||
data: {name, auto_init: autoInit},
|
||||
}), 'apiCreateRepo');
|
||||
}
|
||||
|
||||
export async function apiCreateIssue(requestContext: APIRequestContext, owner: string, repo: string, {title, headers}: {title: string; headers?: Record<string, string>}) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues`, {
|
||||
headers: headers || apiHeaders(),
|
||||
data: {title},
|
||||
}), 'apiCreateIssue');
|
||||
}
|
||||
|
||||
export async function apiStartStopwatch(requestContext: APIRequestContext, owner: string, repo: string, issueIndex: number, {headers}: {headers?: Record<string, string>} = {}) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues/${issueIndex}/stopwatch/start`, {
|
||||
headers: headers || apiHeaders(),
|
||||
}), 'apiStartStopwatch');
|
||||
}
|
||||
|
||||
export async function apiCreateFile(requestContext: APIRequestContext, owner: string, repo: string, filepath: string, content: string) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/contents/${filepath}`, {
|
||||
headers: apiHeaders(),
|
||||
data: {content: globalThis.btoa(content)},
|
||||
}), 'apiCreateFile');
|
||||
}
|
||||
|
||||
export async function apiCreateBranch(requestContext: APIRequestContext, owner: string, repo: string, newBranch: string) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/branches`, {
|
||||
headers: apiHeaders(),
|
||||
data: {new_branch_name: newBranch},
|
||||
}), 'apiCreateBranch');
|
||||
}
|
||||
|
||||
export async function apiDeleteRepo(requestContext: APIRequestContext, owner: string, name: string) {
|
||||
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/repos/${owner}/${name}`, {
|
||||
headers: apiHeaders(),
|
||||
}), 'apiDeleteRepo');
|
||||
}
|
||||
|
||||
export async function apiDeleteOrg(requestContext: APIRequestContext, name: string) {
|
||||
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/orgs/${name}`, {
|
||||
headers: apiHeaders(),
|
||||
}), 'apiDeleteOrg');
|
||||
}
|
||||
|
||||
/** Password shared by all test users — used for both API user creation and browser login. */
|
||||
const testUserPassword = 'e2e-password!aA1';
|
||||
|
||||
export function apiUserHeaders(username: string) {
|
||||
return apiAuthHeader(username, testUserPassword);
|
||||
}
|
||||
|
||||
export async function apiCreateUser(requestContext: APIRequestContext, username: string) {
|
||||
await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/admin/users`, {
|
||||
headers: apiHeaders(),
|
||||
data: {username, password: testUserPassword, email: `${username}@${env.GITEA_TEST_E2E_DOMAIN}`, must_change_password: false},
|
||||
}), 'apiCreateUser');
|
||||
}
|
||||
|
||||
export async function apiDeleteUser(requestContext: APIRequestContext, username: string) {
|
||||
await apiRetry(() => requestContext.delete(`${baseUrl()}/api/v1/admin/users/${username}?purge=true`, {
|
||||
headers: apiHeaders(),
|
||||
}), 'apiDeleteUser');
|
||||
}
|
||||
|
||||
export async function loginUser(page: Page, username: string) {
|
||||
return login(page, username, testUserPassword);
|
||||
}
|
||||
|
||||
export async function login(page: Page, username = env.GITEA_TEST_E2E_USER, password = env.GITEA_TEST_E2E_PASSWORD) {
|
||||
await page.goto('/user/login');
|
||||
await page.getByLabel('Username or Email Address').fill(username);
|
||||
await page.getByLabel('Password').fill(password);
|
||||
await page.getByRole('button', {name: 'Sign In'}).click();
|
||||
await expect(page.getByRole('link', {name: 'Sign In'})).toBeHidden();
|
||||
}
|
||||
|
||||
export async function assertNoJsError(page: Page) {
|
||||
await expect(page.locator('.js-global-error')).toHaveCount(0);
|
||||
}
|
||||
|
||||
/* asserts the child has no horizontal inset from its parent — catches padding/border anywhere
|
||||
* in between regardless of which element declares it */
|
||||
export async function assertFlushWithParent(child: Locator, parent: Locator) {
|
||||
const [childBox, parentBox] = await Promise.all([child.boundingBox(), parent.boundingBox()]);
|
||||
if (!childBox || !parentBox) throw new Error('boundingBox returned null');
|
||||
expect(childBox.x).toBe(parentBox.x);
|
||||
expect(childBox.width).toBe(parentBox.width);
|
||||
}
|
||||
|
||||
export async function logout(page: Page) {
|
||||
await page.context().clearCookies(); // workaround issues related to fomantic dropdown
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('link', {name: 'Sign In'})).toBeVisible();
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import {expect} from '@playwright/test';
|
||||
import {env} from 'node:process';
|
||||
import type {Browser, Page, WorkerInfo} from '@playwright/test';
|
||||
|
||||
const ARTIFACTS_PATH = `tests/e2e/test-artifacts`;
|
||||
const LOGIN_PASSWORD = 'password';
|
||||
|
||||
// log in user and store session info. This should generally be
|
||||
// run in test.beforeAll(), then the session can be loaded in tests.
|
||||
export async function login_user(browser: Browser, workerInfo: WorkerInfo, user: string) {
|
||||
// Set up a new context
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// Route to login page
|
||||
// Note: this could probably be done more quickly with a POST
|
||||
const response = await page.goto('/user/login');
|
||||
expect(response?.status()).toBe(200); // Status OK
|
||||
|
||||
// Fill out form
|
||||
await page.locator('input[name=user_name]').fill(user);
|
||||
await page.locator('input[name=password]').fill(LOGIN_PASSWORD);
|
||||
await page.click('form button.ui.primary.button:visible');
|
||||
|
||||
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
|
||||
|
||||
expect(page.url(), {message: `Failed to login user ${user}`}).toBe(`${workerInfo.project.use.baseURL}/`);
|
||||
|
||||
// Save state
|
||||
await context.storageState({path: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function load_logged_in_context(browser: Browser, workerInfo: WorkerInfo, user: string) {
|
||||
let context;
|
||||
try {
|
||||
context = await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
throw new Error(`Could not find state for '${user}'. Did you call login_user(browser, workerInfo, '${user}') in test.beforeAll()?`);
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export async function save_visual(page: Page) {
|
||||
// Optionally include visual testing
|
||||
if (env.VISUAL_TEST) {
|
||||
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
|
||||
// Mock page/version string
|
||||
await page.locator('footer div.ui.left').evaluate((node) => node.innerHTML = 'MOCK');
|
||||
await expect(page).toHaveScreenshot({
|
||||
fullPage: true,
|
||||
timeout: 20000,
|
||||
mask: [
|
||||
page.locator('.secondary-nav span>img.ui.avatar'),
|
||||
page.locator('.ui.dropdown.jump.item span>img.ui.avatar'),
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func onGiteaRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare ...bool) {
|
||||
if len(prepare) == 0 || prepare[0] {
|
||||
defer tests.PrepareTestEnv(t, 1)()
|
||||
}
|
||||
s := http.Server{
|
||||
Handler: testE2eWebRoutes,
|
||||
}
|
||||
|
||||
u, err := url.Parse(setting.AppURL)
|
||||
assert.NoError(t, err)
|
||||
listener, err := net.Listen("tcp", u.Host)
|
||||
i := 0
|
||||
for err != nil && i <= 10 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
listener, err = net.Listen("tcp", u.Host)
|
||||
i++
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
u.Host = listener.Addr().String()
|
||||
|
||||
defer func() {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
|
||||
s.Shutdown(ctx)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
go s.Serve(listener)
|
||||
// Started by config go ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs)
|
||||
|
||||
callback(t, u)
|
||||
}
|
||||
|
||||
func onGiteaRun(t *testing.T, callback func(*testing.T, *url.URL), prepare ...bool) {
|
||||
onGiteaRunTB(t, func(t testing.TB, u *url.URL) {
|
||||
callback(t.(*testing.T), u)
|
||||
}, prepare...)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestApproveAllRunsOnPullRequestPage(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
// user2 is the owner of the base repo
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2Session := loginUser(t, user2.Name)
|
||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
// user4 is the owner of the fork repo
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user4Session := loginUser(t, user4.Name)
|
||||
user4Token := getTokenForLoggedInUser(t, loginUser(t, user4.Name), auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiBaseRepo := createActionsTestRepo(t, user2Token, "approve-all-runs", false)
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiBaseRepo.ID})
|
||||
user2APICtx := NewAPITestContext(t, baseRepo.OwnerName, baseRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(user2APICtx)(t)
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, baseRepo.OwnerName, baseRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
// init workflows
|
||||
wf1TreePath := ".gitea/workflows/pull_1.yml"
|
||||
wf1FileContent := `name: Pull 1
|
||||
on: pull_request
|
||||
jobs:
|
||||
unit-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo unit-test
|
||||
`
|
||||
opts1 := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create %s"+wf1TreePath, wf1FileContent)
|
||||
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wf1TreePath, opts1)
|
||||
wf2TreePath := ".gitea/workflows/pull_2.yml"
|
||||
wf2FileContent := `name: Pull 2
|
||||
on: pull_request
|
||||
jobs:
|
||||
integration-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo integration-test
|
||||
`
|
||||
opts2 := getWorkflowCreateFileOptions(user2, baseRepo.DefaultBranch, "create %s"+wf2TreePath, wf2FileContent)
|
||||
createWorkflowFile(t, user2Token, baseRepo.OwnerName, baseRepo.Name, wf2TreePath, opts2)
|
||||
|
||||
// user4 forks the repo
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
|
||||
&api.CreateForkOption{
|
||||
Name: new("approve-all-runs-fork"),
|
||||
}).AddTokenAuth(user4Token)
|
||||
resp := MakeRequest(t, req, http.StatusAccepted)
|
||||
var apiForkRepo api.Repository
|
||||
DecodeJSON(t, resp, &apiForkRepo)
|
||||
forkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiForkRepo.ID})
|
||||
user4APICtx := NewAPITestContext(t, user4.Name, forkRepo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
defer doAPIDeleteRepository(user4APICtx)(t)
|
||||
|
||||
// user4 creates a pull request from branch "bugfix/user4"
|
||||
doAPICreateFile(user4APICtx, "user4-fix.txt", &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
NewBranchName: "bugfix/user4",
|
||||
Message: "create user4-fix.txt",
|
||||
Author: api.Identity{
|
||||
Name: user4.Name,
|
||||
Email: user4.Email,
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: user4.Name,
|
||||
Email: user4.Email,
|
||||
},
|
||||
Dates: api.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte("user4-fix")),
|
||||
})(t)
|
||||
apiPull, err := doAPICreatePullRequest(user4APICtx, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, user4.Name+":bugfix/user4")(t)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check runs
|
||||
run1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, WorkflowID: "pull_1.yml"})
|
||||
assert.True(t, run1.NeedApproval)
|
||||
assert.Equal(t, actions_model.StatusBlocked, run1.Status)
|
||||
run2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: baseRepo.ID, TriggerUserID: user4.ID, WorkflowID: "pull_2.yml"})
|
||||
assert.True(t, run2.NeedApproval)
|
||||
assert.Equal(t, actions_model.StatusBlocked, run2.Status)
|
||||
|
||||
// user4 cannot see the approve button
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", baseRepo.OwnerName, baseRepo.Name, apiPull.Index))
|
||||
resp = user4Session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
assert.Zero(t, htmlDoc.doc.Find("#approve-status-checks button.link-action").Length())
|
||||
|
||||
// user2 can see the approve button
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", baseRepo.OwnerName, baseRepo.Name, apiPull.Index))
|
||||
resp = user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
dataURL, exist := htmlDoc.doc.Find("#approve-status-checks button.link-action").Attr("data-url")
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t,
|
||||
fmt.Sprintf("%s/actions/approve-all-checks?commit_id=%s",
|
||||
baseRepo.Link(), apiPull.Head.Sha),
|
||||
dataURL,
|
||||
)
|
||||
|
||||
// user2 approves all runs
|
||||
req = NewRequest(t, "POST", dataURL)
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// check runs
|
||||
run1 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run1.ID})
|
||||
assert.False(t, run1.NeedApproval)
|
||||
assert.Equal(t, user2.ID, run1.ApprovedBy)
|
||||
assert.Equal(t, actions_model.StatusWaiting, run1.Status)
|
||||
run2 = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: run2.ID})
|
||||
assert.False(t, run2.NeedApproval)
|
||||
assert.Equal(t, user2.ID, run2.ApprovedBy)
|
||||
assert.Equal(t, actions_model.StatusWaiting, run2.Status)
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -121,59 +122,55 @@ jobs:
|
||||
opts := getWorkflowCreateFileOptions(user2, apiRepo.DefaultBranch, "create "+testCase.treePath, testCase.fileContent)
|
||||
createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts)
|
||||
|
||||
runIndex := ""
|
||||
var runID int64
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
task := runner.fetchTask(t)
|
||||
jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id)
|
||||
outcome := testCase.outcomes[jobName]
|
||||
assert.NotNil(t, outcome)
|
||||
runner.execTask(t, task, outcome)
|
||||
runIndex = task.Context.GetFields()["run_number"].GetStringValue()
|
||||
assert.Equal(t, "1", runIndex)
|
||||
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
|
||||
parsedRunIndex, err := strconv.ParseInt(runIndex, 10, 64)
|
||||
assert.NoError(t, err)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: apiRepo.ID, Index: parsedRunIndex})
|
||||
runID = run.ID
|
||||
}
|
||||
|
||||
jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
jobID := jobs[i].ID
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, apiRepo.Name, runID, jobID))
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
var listResp actions.ViewResponse
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, listResp.State.Run.Jobs, 3)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)).
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, apiRepo.Name, runID, jobID)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
req := NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, apiRepo.Name, runID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/delete", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID))
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequestWithValues(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s", user2.Name, apiRepo.Name, runIndex), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, apiRepo.Name, runID))
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
for i := 0; i < len(testCase.outcomes); i++ {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d", user2.Name, apiRepo.Name, runIndex, i), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
jobID := jobs[i].ID
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, apiRepo.Name, runID, jobID))
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, apiRepo.Name, runIndex, i)).
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, apiRepo.Name, runID, jobID)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
@@ -62,9 +62,8 @@ jobs:
|
||||
// run the workflow with os=windows
|
||||
urlStr := fmt.Sprintf("/%s/%s/actions/run?workflow=%s", user2.Name, repo.Name, "test-inputs-context.yml")
|
||||
req := NewRequestWithValues(t, "POST", urlStr, map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
"ref": "refs/heads/main",
|
||||
"os": "windows",
|
||||
"ref": "refs/heads/main",
|
||||
"os": "windows",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestJobWithNeeds(t *testing.T) {
|
||||
@@ -349,6 +350,122 @@ jobs:
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunnerDisableEnable(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
t.Run("BasicDisableEnable", func(t *testing.T) {
|
||||
testData := prepareRunnerDisableEnableTest(t, user2, token, "actions-runner-disable-enable", "mock-runner", "runner-disable-enable")
|
||||
|
||||
task1 := testData.runner.fetchTask(t)
|
||||
require.NotNil(t, task1)
|
||||
|
||||
triggerRunnerDisableEnableRun(t, user2, token, testData.repo, "second-push.txt")
|
||||
|
||||
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
testData.runner.execTask(t, task1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
testData.runner.fetchNoTask(t, 2*time.Second)
|
||||
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
task2 := testData.runner.fetchTask(t, 5*time.Second)
|
||||
require.NotNil(t, task2)
|
||||
testData.runner.execTask(t, task2, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
})
|
||||
|
||||
t.Run("TasksVersionPath", func(t *testing.T) {
|
||||
testData := prepareRunnerDisableEnableTest(t, user2, token, "actions-runner-version-path", "mock-runner-version-path", "runner-version-path")
|
||||
|
||||
var firstVersion int64
|
||||
var task1 *runnerv1.Task
|
||||
ddl := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(ddl) {
|
||||
task1, firstVersion = testData.runner.fetchTaskOnce(t, 0)
|
||||
if task1 != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
require.NotNil(t, task1, "expected to receive first task")
|
||||
require.NotZero(t, firstVersion, "response TasksVersion should be set")
|
||||
|
||||
// Trigger a second run so there is a pending job after we re-enable the runner
|
||||
triggerRunnerDisableEnableRun(t, user2, token, testData.repo, "second-push.txt")
|
||||
time.Sleep(500 * time.Millisecond) // allow workflow run to be created
|
||||
|
||||
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
testData.runner.execTask(t, task1, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
|
||||
// Fetch with the version we had before disable. Server has bumped version on disable,
|
||||
// so we enter PickTask with a re-loaded runner (disabled) and get no task.
|
||||
taskAfterDisable, _ := testData.runner.fetchTaskOnce(t, firstVersion)
|
||||
assert.Nil(t, taskAfterDisable, "disabled runner must not receive a task when sending previous TasksVersion")
|
||||
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/%d", user2.Name, testData.repo.Name, testData.runnerID), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
task2 := testData.runner.fetchTask(t, 5*time.Second)
|
||||
require.NotNil(t, task2, "after re-enable runner should receive tasks again")
|
||||
testData.runner.execTask(t, task2, &mockTaskOutcome{result: runnerv1.Result_RESULT_SUCCESS})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type runnerDisableEnableTestData struct {
|
||||
repo *api.Repository
|
||||
runner *mockRunner
|
||||
runnerID int64
|
||||
}
|
||||
|
||||
func prepareRunnerDisableEnableTest(t *testing.T, user *user_model.User, authToken, repoName, runnerName, workflowName string) *runnerDisableEnableTestData {
|
||||
t.Helper()
|
||||
|
||||
apiRepo := createActionsTestRepo(t, authToken, repoName, false)
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, user.Name, apiRepo.Name, runnerName, []string{"ubuntu-latest"}, false)
|
||||
|
||||
wfTreePath := fmt.Sprintf(".gitea/workflows/%s.yml", workflowName)
|
||||
wfContent := fmt.Sprintf(`name: %s
|
||||
on: [push]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo %s
|
||||
`, workflowName, workflowName)
|
||||
opts := getWorkflowCreateFileOptions(user, apiRepo.DefaultBranch, "create workflow", wfContent)
|
||||
createWorkflowFile(t, authToken, user.Name, apiRepo.Name, wfTreePath, opts)
|
||||
|
||||
return &runnerDisableEnableTestData{
|
||||
repo: apiRepo,
|
||||
runner: runner,
|
||||
runnerID: getRepoRunnerID(t, authToken, user.Name, apiRepo.Name),
|
||||
}
|
||||
}
|
||||
|
||||
func triggerRunnerDisableEnableRun(t *testing.T, user *user_model.User, authToken string, repo *api.Repository, treePath string) {
|
||||
t.Helper()
|
||||
opts := getWorkflowCreateFileOptions(user, repo.DefaultBranch, "second push", "second run")
|
||||
createWorkflowFile(t, authToken, user.Name, repo.Name, treePath, opts)
|
||||
}
|
||||
|
||||
func getRepoRunnerID(t *testing.T, authToken, ownerName, repoName string) int64 {
|
||||
t.Helper()
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners", ownerName, repoName)).AddTokenAuth(authToken)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
runnerList := api.ActionRunnersResponse{}
|
||||
DecodeJSON(t, resp, &runnerList)
|
||||
require.Len(t, runnerList.Entries, 1)
|
||||
return runnerList.Entries[0].ID
|
||||
}
|
||||
|
||||
func TestActionsGiteaContext(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsJobTokenPermissiveAccess(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
isFork bool
|
||||
|
||||
ownerPermMode repo_model.ActionsTokenPermissionMode
|
||||
ownerMaxPerms map[unit_model.Type]perm.AccessMode
|
||||
|
||||
repoPermMode repo_model.ActionsTokenPermissionMode
|
||||
repoMaxPerms map[unit_model.Type]perm.AccessMode
|
||||
|
||||
expectGitAccess perm.AccessMode
|
||||
}{
|
||||
{
|
||||
name: "OwnerConfig-Permissive",
|
||||
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
|
||||
expectGitAccess: perm.AccessModeWrite,
|
||||
},
|
||||
{
|
||||
name: "OwnerConfig-Permissive-CodeNone",
|
||||
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
|
||||
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
|
||||
expectGitAccess: perm.AccessModeNone,
|
||||
},
|
||||
{
|
||||
name: "OwnerConfig-Restricted",
|
||||
ownerPermMode: repo_model.ActionsTokenPermissionModeRestricted,
|
||||
expectGitAccess: perm.AccessModeRead,
|
||||
},
|
||||
|
||||
// repo uses its own settings, so owner settings should not affect it
|
||||
{
|
||||
name: "SameRepo-Permissive",
|
||||
ownerPermMode: repo_model.ActionsTokenPermissionModeRestricted,
|
||||
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
|
||||
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
|
||||
expectGitAccess: perm.AccessModeWrite,
|
||||
},
|
||||
{
|
||||
name: "SameRepo-Permissive-CodeNone",
|
||||
ownerPermMode: repo_model.ActionsTokenPermissionModePermissive,
|
||||
ownerMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeRead},
|
||||
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
|
||||
repoMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
|
||||
expectGitAccess: perm.AccessModeNone,
|
||||
},
|
||||
{
|
||||
name: "SameRepo-Restricted",
|
||||
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
|
||||
expectGitAccess: perm.AccessModeRead,
|
||||
},
|
||||
|
||||
// forks should be always restricted to max read access for code
|
||||
{
|
||||
name: "Fork-Permissive",
|
||||
repoPermMode: repo_model.ActionsTokenPermissionModePermissive,
|
||||
isFork: true,
|
||||
expectGitAccess: perm.AccessModeRead,
|
||||
},
|
||||
{
|
||||
name: "Fork-Restricted",
|
||||
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
|
||||
isFork: true,
|
||||
expectGitAccess: perm.AccessModeRead,
|
||||
},
|
||||
{
|
||||
name: "Fork-Restricted-CodeNone",
|
||||
repoPermMode: repo_model.ActionsTokenPermissionModeRestricted,
|
||||
repoMaxPerms: map[unit_model.Type]perm.AccessMode{unit_model.TypeCode: perm.AccessModeNone},
|
||||
isFork: true,
|
||||
expectGitAccess: perm.AccessModeNone,
|
||||
},
|
||||
}
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 47})
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: task.RepoID})
|
||||
repoActionsUnit := repo.MustGetUnit(t.Context(), unit_model.TypeActions)
|
||||
repoActionsCfg := repoActionsUnit.ActionsConfig()
|
||||
ownerActionsCfg, err := actions_model.GetOwnerActionsConfig(t.Context(), repo.OwnerID)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.GetEngine(t.Context()).ID(task.RepoID).Cols("is_private").Update(&repo_model.Repository{IsPrivate: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
assertRespCodeForSuccess := func(t *testing.T, resp *httptest.ResponseRecorder, succeed bool) {
|
||||
if succeed {
|
||||
assert.True(t, 200 <= resp.Code && resp.Code < 300, "Expected success status code, got %d", resp.Code)
|
||||
} else {
|
||||
assert.True(t, 400 <= resp.Code && resp.Code < 500, "Expected client error status code, got %d", resp.Code)
|
||||
}
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// prepare owner's token permissions settings
|
||||
ownerActionsCfg.TokenPermissionMode = tt.ownerPermMode
|
||||
ownerActionsCfg.MaxTokenPermissions = util.Iif(tt.ownerMaxPerms == nil, nil, &repo_model.ActionsTokenPermissions{UnitAccessModes: tt.ownerMaxPerms})
|
||||
require.NoError(t, actions_model.SetOwnerActionsConfig(t.Context(), repo.OwnerID, ownerActionsCfg))
|
||||
|
||||
// prepare repo's token permissions settings
|
||||
repoActionsCfg.OverrideOwnerConfig = tt.repoPermMode != "" || tt.repoMaxPerms != nil
|
||||
repoActionsCfg.TokenPermissionMode = tt.repoPermMode
|
||||
repoActionsCfg.MaxTokenPermissions = util.Iif(tt.repoMaxPerms == nil, nil, &repo_model.ActionsTokenPermissions{UnitAccessModes: tt.repoMaxPerms})
|
||||
require.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoActionsUnit))
|
||||
|
||||
// prepare task and its token
|
||||
require.NoError(t, task.GenerateToken())
|
||||
task.Status = actions_model.StatusRunning
|
||||
task.IsForkPullRequest = tt.isFork
|
||||
err := actions_model.UpdateTask(t.Context(), task, "token_hash", "token_salt", "token_last_eight", "status", "is_fork_pull_request")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, task.LoadJob(t.Context()))
|
||||
require.NoError(t, task.Job.LoadRun(t.Context()))
|
||||
task.Job.Run.IsForkPullRequest = tt.isFork
|
||||
require.NoError(t, actions_model.UpdateRun(t.Context(), task.Job.Run, "is_fork_pull_request"))
|
||||
|
||||
testURL := *u
|
||||
testURL.User = url.UserPassword("gitea-actions", task.Token)
|
||||
|
||||
t.Run("ReadGitContent", func(t *testing.T) {
|
||||
testURL.Path = "/user5/repo4.git/HEAD"
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", testURL.String()), NoExpectedStatus)
|
||||
assertRespCodeForSuccess(t, resp, tt.expectGitAccess != perm.AccessModeNone)
|
||||
|
||||
testURL.Path = "/user5/repo4.git/info/lfs/locks"
|
||||
req := NewRequest(t, "GET", testURL.String()).SetHeader("Accept", lfs.MediaType)
|
||||
resp = MakeRequest(t, req, NoExpectedStatus)
|
||||
assertRespCodeForSuccess(t, resp, tt.expectGitAccess != perm.AccessModeNone)
|
||||
})
|
||||
|
||||
t.Run("WriteGitContent", func(t *testing.T) {
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/contents/test-filename", repo.FullName()), &structs.CreateFileOptions{
|
||||
FileOptions: structs.FileOptions{NewBranchName: "new-branch" + t.Name()},
|
||||
ContentBase64: base64.StdEncoding.EncodeToString([]byte(`dummy content`)),
|
||||
}).AddTokenAuth(task.Token)
|
||||
resp := MakeRequest(t, req, NoExpectedStatus)
|
||||
assertRespCodeForSuccess(t, resp, tt.expectGitAccess == perm.AccessModeWrite)
|
||||
|
||||
testURL.Path = "/user5/repo4.git/info/lfs/objects/batch"
|
||||
req = NewRequestWithJSON(t, "POST", testURL.String(), lfs.BatchRequest{Operation: "upload"}).SetHeader("Accept", lfs.MediaType)
|
||||
resp = MakeRequest(t, req, NoExpectedStatus)
|
||||
assertRespCodeForSuccess(t, resp, tt.expectGitAccess == perm.AccessModeWrite)
|
||||
})
|
||||
|
||||
t.Run("NoOtherPermissions", func(t *testing.T) {
|
||||
req := NewRequest(t, "DELETE", "/api/v1/repos/"+repo.FullName()).AddTokenAuth(task.Token)
|
||||
resp := MakeRequest(t, req, NoExpectedStatus)
|
||||
assertRespCodeForSuccess(t, resp, false)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionsCrossRepoAccess(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
session := loginUser(t, "user2")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
|
||||
|
||||
// 1. Create Organization
|
||||
orgName := "org-cross-test"
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &structs.CreateOrgOption{
|
||||
UserName: orgName,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
owner, err := org_model.GetOrgByName(t.Context(), orgName)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 2. Create Two Repositories in owner
|
||||
createRepoInOrg := func(name string) int64 {
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/repos", orgName), &structs.CreateRepoOption{
|
||||
Name: name,
|
||||
AutoInit: true,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
var repo structs.Repository
|
||||
DecodeJSON(t, resp, &repo)
|
||||
return repo.ID
|
||||
}
|
||||
|
||||
repoAID := createRepoInOrg("repo-A")
|
||||
repoBID := createRepoInOrg("repo-B")
|
||||
|
||||
// 3. Enable Actions in Repo A (Source) and Repo B (Target)
|
||||
enableActions := func(repoID int64) {
|
||||
err := db.Insert(t.Context(), &repo_model.RepoUnit{
|
||||
RepoID: repoID,
|
||||
Type: unit_model.TypeActions,
|
||||
Config: &repo_model.ActionsConfig{
|
||||
TokenPermissionMode: repo_model.ActionsTokenPermissionModePermissive,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
enableActions(repoAID)
|
||||
enableActions(repoBID)
|
||||
|
||||
// 4. Create Task in Repo A, and use A's token to access B
|
||||
taskA := createActionTask(t, repoAID, false)
|
||||
testCtxA := APITestContext{
|
||||
Session: emptyTestSession(t),
|
||||
Token: taskA.Token,
|
||||
Username: orgName,
|
||||
Reponame: "repo-B",
|
||||
}
|
||||
|
||||
testCtxA.ExpectedCode = http.StatusOK
|
||||
t.Run("PublicCrossRepoAccess", doAPIGetRepository(testCtxA, func(t *testing.T, r structs.Repository) {
|
||||
assert.Equal(t, "repo-B", r.Name)
|
||||
}))
|
||||
|
||||
// make repo-B be private
|
||||
req = NewRequestWithJSON(t, "PATCH", "/api/v1/repos/org-cross-test/repo-B", &structs.EditRepoOption{Private: new(true)}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
testCtxA.ExpectedCode = http.StatusNotFound
|
||||
t.Run("NoPrivateCrossRepoAccess", doAPIGetRepository(testCtxA, nil))
|
||||
|
||||
ownerActionsCfg := actions_model.OwnerActionsConfig{AllowedCrossRepoIDs: []int64{repoBID}}
|
||||
require.NoError(t, actions_model.SetOwnerActionsConfig(t.Context(), owner.ID, ownerActionsCfg))
|
||||
|
||||
testCtxA.ExpectedCode = http.StatusOK
|
||||
t.Run("AccessToSelectedPrivateRepo", doAPIGetRepository(testCtxA, func(t *testing.T, r structs.Repository) {
|
||||
assert.Equal(t, "repo-B", r.Name)
|
||||
}))
|
||||
|
||||
t.Run("RepoTransfer", func(t *testing.T) {
|
||||
ownerActionsCfg, err := actions_model.GetOwnerActionsConfig(t.Context(), owner.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, ownerActionsCfg.AllowedCrossRepoIDs, repoBID)
|
||||
|
||||
// Transfer Repository to user4
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/repo-B/transfer", orgName), &structs.TransferRepoOption{
|
||||
NewOwner: "user4",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// Accept transfer as user4
|
||||
session4 := loginUser(t, "user4")
|
||||
token4 := getTokenForLoggedInUser(t, session4, auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/repo-B/transfer/accept", orgName)).AddTokenAuth(token4)
|
||||
MakeRequest(t, req, http.StatusAccepted)
|
||||
|
||||
// Verify it is removed from the org's config
|
||||
ownerActionsCfg, err = actions_model.GetOwnerActionsConfig(t.Context(), owner.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, ownerActionsCfg.AllowedCrossRepoIDs, repoBID)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestActionsJobTokenPermissions(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("WriteIssue", TestActionsJobTokenPermissionsWriteIssue)
|
||||
}
|
||||
|
||||
func TestActionsJobTokenPermissionsWriteIssue(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 53})
|
||||
require.Equal(t, repo.ID, task.RepoID)
|
||||
|
||||
require.NoError(t, db.Insert(t.Context(), &repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
Type: unit_model.TypeActions,
|
||||
Config: &repo_model.ActionsConfig{},
|
||||
}))
|
||||
|
||||
repoActionsUnit := repo.MustGetUnit(t.Context(), unit_model.TypeActions)
|
||||
repoActionsCfg := repoActionsUnit.ActionsConfig()
|
||||
repoActionsCfg.OverrideOwnerConfig = true
|
||||
repoActionsCfg.TokenPermissionMode = repo_model.ActionsTokenPermissionModePermissive
|
||||
repoActionsCfg.MaxTokenPermissions = nil
|
||||
require.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoActionsUnit))
|
||||
|
||||
require.NoError(t, task.GenerateToken())
|
||||
task.Status = actions_model.StatusRunning
|
||||
require.NoError(t, actions_model.UpdateTask(t.Context(), task, "token_hash", "token_salt", "token_last_eight", "status"))
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
labelURL := fmt.Sprintf("/api/v1/repos/%s/%s/labels", user.Name, repo.Name)
|
||||
req := NewRequestWithJSON(t, "POST", labelURL, &structs.CreateLabelOption{
|
||||
Name: "task-label",
|
||||
Color: "0e8a16",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
label := DecodeJSON(t, resp, &structs.Label{})
|
||||
|
||||
issueURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues", user.Name, repo.Name)
|
||||
req = NewRequestWithJSON(t, "POST", issueURL, &structs.CreateIssueOption{
|
||||
Title: "issue for actions token label deletion",
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
issue := DecodeJSON(t, resp, &structs.Issue{})
|
||||
|
||||
taskToken := task.Token
|
||||
require.NotEmpty(t, taskToken)
|
||||
|
||||
issueLabelsURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/labels", user.Name, repo.Name, issue.Index)
|
||||
req = NewRequestWithJSON(t, "POST", issueLabelsURL, &structs.IssueLabelsOption{
|
||||
Labels: []any{label.ID},
|
||||
}).AddTokenAuth(taskToken)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%d", issueLabelsURL, label.ID)).AddTokenAuth(taskToken)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func createActionTask(t *testing.T, repoID int64, isFork bool) *actions_model.ActionTask {
|
||||
job := &actions_model.ActionRunJob{
|
||||
RepoID: repoID,
|
||||
Status: actions_model.StatusRunning,
|
||||
IsForkPullRequest: isFork,
|
||||
JobID: "test_job",
|
||||
Name: "test_job",
|
||||
}
|
||||
require.NoError(t, db.Insert(t.Context(), job))
|
||||
task := &actions_model.ActionTask{
|
||||
JobID: job.ID,
|
||||
RepoID: repoID,
|
||||
Status: actions_model.StatusRunning,
|
||||
IsForkPullRequest: isFork,
|
||||
}
|
||||
require.NoError(t, task.GenerateToken())
|
||||
require.NoError(t, db.Insert(t.Context(), task))
|
||||
return task
|
||||
}
|
||||
|
||||
func TestActionsTokenPermissionsPersistenceWithWorkflow(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
// create repos
|
||||
repo1 := createActionsTestRepo(t, token, "actions-permission-repo1", false)
|
||||
repo2 := createActionsTestRepo(t, token, "actions-permission-repo2", true)
|
||||
|
||||
// add repo2 to owner-level cross-repo access list
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings/actions/general", map[string]string{
|
||||
"cross_repo_add_target": "true",
|
||||
"cross_repo_add_target_name": repo2.Name,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// create the runner for repo1
|
||||
runner1 := newMockRunner()
|
||||
runner1.registerAsRepoRunner(t, user2.Name, repo1.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
// set repo1 actions token permission mode to "permissive"
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo1.Name), map[string]string{
|
||||
"token_permission_mode": "permissive",
|
||||
"override_owner_config": "true",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
// set repo2 actions token permission mode to "restricted", and set max permissions
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
|
||||
"token_permission_mode": "restricted",
|
||||
"override_owner_config": "true",
|
||||
"enable_max_permissions": "true",
|
||||
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeReleases)): "read",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
// create a workflow file with "permission" keyword for repo1
|
||||
wfTreePath := ".gitea/workflows/test_permissions.yml"
|
||||
wfFileContent := `name: Test Permissions
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/test_permissions.yml'
|
||||
|
||||
jobs:
|
||||
job-override:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
code: write
|
||||
steps:
|
||||
- run: echo "test perms"
|
||||
`
|
||||
opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+wfTreePath, wfFileContent)
|
||||
createWorkflowFile(t, token, user2.Name, repo1.Name, wfTreePath, opts)
|
||||
|
||||
task1 := runner1.fetchTask(t)
|
||||
task1Token := task1.Secrets["GITEA_TOKEN"]
|
||||
require.NotEmpty(t, task1Token)
|
||||
|
||||
// should fail: target repo does not allow code access
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo2.Name)).AddTokenAuth(task1Token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// set repo2 max permission to "read" so that the actions token can access code
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
|
||||
"token_permission_mode": "restricted",
|
||||
"override_owner_config": "true",
|
||||
"enable_max_permissions": "true",
|
||||
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "read",
|
||||
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeReleases)): "read",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
// should succeed: target repo now allows code read access for this token
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo2.Name)).AddTokenAuth(task1Token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
// but it should not have write access
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}).
|
||||
SetHeader("Accept", lfs.MediaType).
|
||||
AddBasicAuth("gitea-actions", task1Token)
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
// set repo1&repo2 max permission to "write" so that the actions token can access code
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo1.Name), map[string]string{
|
||||
"token_permission_mode": "restricted",
|
||||
"override_owner_config": "true",
|
||||
"enable_max_permissions": "true",
|
||||
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "write",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/token_permissions", user2.Name, repo2.Name), map[string]string{
|
||||
"token_permission_mode": "restricted",
|
||||
"override_owner_config": "true",
|
||||
"enable_max_permissions": "true",
|
||||
"max_unit_access_mode_" + strconv.Itoa(int(unit_model.TypeCode)): "write",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
// now task1 has write access to repo1, but still only read access to repo2 (different repo)
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo1.Name), lfs.BatchRequest{Operation: "upload"}).
|
||||
SetHeader("Accept", lfs.MediaType).
|
||||
AddBasicAuth("gitea-actions", task1Token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/%s/%s.git/info/lfs/objects/batch", user2.Name, repo2.Name), lfs.BatchRequest{Operation: "upload"}).
|
||||
SetHeader("Accept", lfs.MediaType).
|
||||
AddBasicAuth("gitea-actions", task1Token)
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/dbfs"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
actions_module "code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Regression for https://gitea.com/gitea/runner/issues/950: a runner that
|
||||
// finalizes a task with no log output sends UpdateLog{Rows:[], NoMore:true}.
|
||||
// The previous short-circuit on len(Rows)==0 skipped TransferLogs, leaving
|
||||
// an orphan dbfs_data row. Verify the row is now archived and removed.
|
||||
func TestActionsLogFinalizeWithoutRows(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
session := loginUser(t, user2.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
apiRepo := createActionsTestRepo(t, token, "actions-finalize-no-rows", false)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID})
|
||||
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, user2.Name, repo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
const wfTreePath = ".gitea/workflows/finalize-no-rows.yml"
|
||||
wfFileContent := fmt.Sprintf(`name: finalize-no-rows
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '%s'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: noop
|
||||
`, wfTreePath)
|
||||
createWorkflowFile(t, token, user2.Name, repo.Name, wfTreePath, getWorkflowCreateFileOptions(user2, repo.DefaultBranch, "trigger", wfFileContent))
|
||||
|
||||
task := runner.fetchTask(t)
|
||||
|
||||
resp, err := runner.client.runnerServiceClient.UpdateLog(t.Context(), connect.NewRequest(&runnerv1.UpdateLogRequest{
|
||||
TaskId: task.Id,
|
||||
Index: 0,
|
||||
Rows: nil,
|
||||
NoMore: true,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 0, resp.Msg.AckIndex)
|
||||
|
||||
freshTask := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: task.Id})
|
||||
require.True(t, freshTask.LogInStorage, "log_in_storage must flip after empty NoMore=true")
|
||||
|
||||
_, err = storage.Actions.Stat(freshTask.LogFilename)
|
||||
assert.NoError(t, err, "archived log must exist in storage")
|
||||
|
||||
_, err = dbfs.Open(t.Context(), actions_module.DBFSPrefix+freshTask.LogFilename)
|
||||
assert.ErrorIs(t, err, os.ErrNotExist, "DBFS row must be cleaned up after TransferLogs")
|
||||
})
|
||||
}
|
||||
@@ -7,12 +7,10 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
@@ -171,7 +169,7 @@ jobs:
|
||||
createWorkflowFile(t, token, user2.Name, repo.Name, tc.treePath, opts)
|
||||
|
||||
// fetch and execute tasks
|
||||
for jobIndex, outcome := range tc.outcome {
|
||||
for _, outcome := range tc.outcome {
|
||||
task := runner.fetchTask(t)
|
||||
runner.execTask(t, task, outcome)
|
||||
|
||||
@@ -183,9 +181,10 @@ jobs:
|
||||
_, err := storage.Actions.Stat(logFileName)
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, job, run := getTaskAndJobAndRunByTaskID(t, task.Id)
|
||||
|
||||
// download task logs and check content
|
||||
runIndex := task.Context.GetFields()["run_number"].GetStringValue()
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%s/jobs/%d/logs", user2.Name, repo.Name, runIndex, jobIndex)).
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/logs", user2.Name, repo.Name, run.ID, job.ID)).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
logTextLines := strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
|
||||
@@ -198,15 +197,8 @@ jobs:
|
||||
)
|
||||
}
|
||||
|
||||
runID, _ := strconv.ParseInt(task.Context.GetFields()["run_id"].GetStringValue(), 10, 64)
|
||||
|
||||
jobs, err := actions_model.GetRunJobsByRunID(t.Context(), runID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, jobs, len(tc.outcome))
|
||||
jobID := jobs[jobIndex].ID
|
||||
|
||||
// download task logs from API and check content
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, jobID)).
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/jobs/%d/logs", user2.Name, repo.Name, job.ID)).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
logTextLines = strings.Split(strings.TrimSpace(resp.Body.String()), "\n")
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestActionsRerun(t *testing.T) {
|
||||
|
||||
wfTreePath := ".gitea/workflows/actions-rerun-workflow-1.yml"
|
||||
wfFileContent := `name: actions-rerun-workflow-1
|
||||
on:
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/actions-rerun-workflow-1.yml'
|
||||
@@ -54,25 +54,22 @@ jobs:
|
||||
|
||||
// fetch and exec job1
|
||||
job1Task := runner.fetchTask(t)
|
||||
_, _, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
|
||||
_, job1, run := getTaskAndJobAndRunByTaskID(t, job1Task.Id)
|
||||
runner.execTask(t, job1Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
// RERUN-FAILURE: the run is not done
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
|
||||
session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
// fetch and exec job2
|
||||
job2Task := runner.fetchTask(t)
|
||||
_, job2, _ := getTaskAndJobAndRunByTaskID(t, job2Task.Id)
|
||||
runner.execTask(t, job2Task, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
|
||||
// RERUN-1: rerun the run
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.Index), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo.Name, run.ID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
// fetch and exec job1
|
||||
job1TaskR1 := runner.fetchTask(t)
|
||||
@@ -86,9 +83,7 @@ jobs:
|
||||
})
|
||||
|
||||
// RERUN-2: rerun job1
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 0), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job1.ID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
// job2 needs job1, so rerunning job1 will also rerun job2
|
||||
// fetch and exec job1
|
||||
@@ -103,9 +98,7 @@ jobs:
|
||||
})
|
||||
|
||||
// RERUN-3: rerun job2
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.Index, 1), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo.Name, run.ID, job2.ID))
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
// only job2 will rerun
|
||||
// fetch and exec job2
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
actions_web "code.gitea.io/gitea/routers/web/repo/actions"
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsRoute(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
t.Run("testActionsRouteForIDBasedURL", testActionsRouteForIDBasedURL)
|
||||
t.Run("testActionsRouteForLegacyIndexBasedURL", testActionsRouteForLegacyIndexBasedURL)
|
||||
})
|
||||
}
|
||||
|
||||
func testActionsRouteForIDBasedURL(t *testing.T) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2Session := loginUser(t, user2.Name)
|
||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
|
||||
repo1 := createActionsTestRepo(t, user2Token, "actions-route-id-url-1", false)
|
||||
runner1 := newMockRunner()
|
||||
runner1.registerAsRepoRunner(t, user2.Name, repo1.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
repo2 := createActionsTestRepo(t, user2Token, "actions-route-id-url-2", false)
|
||||
runner2 := newMockRunner()
|
||||
runner2.registerAsRepoRunner(t, user2.Name, repo2.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
workflowTreePath := ".gitea/workflows/test.yml"
|
||||
workflowContent := `name: test
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.gitea/workflows/test.yml'
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo job1
|
||||
`
|
||||
|
||||
opts := getWorkflowCreateFileOptions(user2, repo1.DefaultBranch, "create "+workflowTreePath, workflowContent)
|
||||
createWorkflowFile(t, user2Token, user2.Name, repo1.Name, workflowTreePath, opts)
|
||||
createWorkflowFile(t, user2Token, user2.Name, repo2.Name, workflowTreePath, opts)
|
||||
|
||||
task1 := runner1.fetchTask(t)
|
||||
_, job1, run1 := getTaskAndJobAndRunByTaskID(t, task1.Id)
|
||||
task2 := runner2.fetchTask(t)
|
||||
_, job2, run2 := getTaskAndJobAndRunByTaskID(t, task2.Id)
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo1.Name, run1.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo1.Name, 999999))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// run1 and job1 belong to repo1, success
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run1.ID, job1.ID))
|
||||
resp := user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
var viewResp actions_web.ViewResponse
|
||||
DecodeJSON(t, resp, &viewResp)
|
||||
assert.Len(t, viewResp.State.Run.Jobs, 1)
|
||||
assert.Equal(t, job1.ID, viewResp.State.Run.Jobs[0].ID)
|
||||
|
||||
// run2 and job2 do not belong to repo1, failure
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run2.ID, job2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run1.ID, job2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo1.Name, run2.ID, job1.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/workflow", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/approve", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/cancel", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/artifacts/test.txt", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/%s/%s/actions/runs/%d/artifacts/test.txt", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// make the tasks complete, then test rerun
|
||||
runner1.execTask(t, task1, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
runner2.execTask(t, task2, &mockTaskOutcome{
|
||||
result: runnerv1.Result_RESULT_SUCCESS,
|
||||
})
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", user2.Name, repo1.Name, run2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run2.ID, job2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run1.ID, job2.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d/rerun", user2.Name, repo1.Name, run2.ID, job1.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func testActionsRouteForLegacyIndexBasedURL(t *testing.T) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
user2Session := loginUser(t, user2.Name)
|
||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
repo := createActionsTestRepo(t, user2Token, "actions-route-legacy-url", false)
|
||||
|
||||
mkRun := func(id, index int64, title, sha string) *actions_model.ActionRun {
|
||||
return &actions_model.ActionRun{
|
||||
ID: id,
|
||||
Index: index,
|
||||
RepoID: repo.ID,
|
||||
OwnerID: user2.ID,
|
||||
Title: title,
|
||||
WorkflowID: "legacy-route.yml",
|
||||
TriggerUserID: user2.ID,
|
||||
Ref: "refs/heads/master",
|
||||
CommitSHA: sha,
|
||||
Status: actions_model.StatusWaiting,
|
||||
}
|
||||
}
|
||||
mkJob := func(id, runID int64, name, sha string) *actions_model.ActionRunJob {
|
||||
return &actions_model.ActionRunJob{
|
||||
ID: id,
|
||||
RunID: runID,
|
||||
RepoID: repo.ID,
|
||||
OwnerID: user2.ID,
|
||||
CommitSHA: sha,
|
||||
Name: name,
|
||||
Status: actions_model.StatusWaiting,
|
||||
}
|
||||
}
|
||||
|
||||
// A small ID-based run/job pair that should always resolve directly.
|
||||
smallIDRun := mkRun(80, 20, "legacy route small id", "aaa001")
|
||||
smallIDJob := mkJob(170, smallIDRun.ID, "legacy-small-job", smallIDRun.CommitSHA)
|
||||
// Another small run used to provide a job ID that belongs to a different run.
|
||||
otherSmallRun := mkRun(90, 30, "legacy route other small", "aaa002")
|
||||
otherSmallJob := mkJob(180, otherSmallRun.ID, "legacy-other-small-job", otherSmallRun.CommitSHA)
|
||||
|
||||
// A large-ID run whose legacy run index should redirect to its ID-based URL.
|
||||
normalRun := mkRun(1500, 900, "legacy route normal", "aaa003")
|
||||
normalRunJob := mkJob(1600, normalRun.ID, "legacy-normal-job", normalRun.CommitSHA)
|
||||
// A run whose index collides with normalRun.ID to exercise summary-page ID-first behavior.
|
||||
collisionRun := mkRun(2400, 1500, "legacy route collision", "aaa004")
|
||||
collisionJobIdx0 := mkJob(2600, collisionRun.ID, "legacy-collision-job-1", collisionRun.CommitSHA)
|
||||
collisionJobIdx1 := mkJob(2601, collisionRun.ID, "legacy-collision-job-2", collisionRun.CommitSHA)
|
||||
|
||||
// A run whose job has a smaller ID than the run itself (job_id < run_id)
|
||||
jobSmallerThanRunRun := mkRun(5000, 5500, "legacy route job before run", "aaa007")
|
||||
jobSmallerThanRunJob := mkJob(4500, jobSmallerThanRunRun.ID, "legacy-job-before-run-job", jobSmallerThanRunRun.CommitSHA)
|
||||
|
||||
// A small ID-based run/job pair that collides with a different legacy run/job index pair.
|
||||
ambiguousIDRun := mkRun(3, 1, "legacy route ambiguous id", "aaa005")
|
||||
ambiguousIDJob := mkJob(4, ambiguousIDRun.ID, "legacy-ambiguous-id-job", ambiguousIDRun.CommitSHA)
|
||||
// The legacy run/job target for the ambiguous /runs/3/jobs/4 URL.
|
||||
ambiguousLegacyRun := mkRun(1501, ambiguousIDRun.ID, "legacy route ambiguous legacy", "aaa006")
|
||||
ambiguousLegacyJobIdx0 := mkJob(1601, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-0", ambiguousLegacyRun.CommitSHA)
|
||||
ambiguousLegacyJobIdx1 := mkJob(1602, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-1", ambiguousLegacyRun.CommitSHA)
|
||||
ambiguousLegacyJobIdx2 := mkJob(1603, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-2", ambiguousLegacyRun.CommitSHA)
|
||||
ambiguousLegacyJobIdx3 := mkJob(1604, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-3", ambiguousLegacyRun.CommitSHA)
|
||||
ambiguousLegacyJobIdx4 := mkJob(1605, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-4", ambiguousLegacyRun.CommitSHA) // job_index=4
|
||||
ambiguousLegacyJobIdx5 := mkJob(1606, ambiguousLegacyRun.ID, "legacy-ambiguous-legacy-job-5", ambiguousLegacyRun.CommitSHA)
|
||||
ambiguousLegacyJobs := []*actions_model.ActionRunJob{
|
||||
ambiguousLegacyJobIdx0,
|
||||
ambiguousLegacyJobIdx1,
|
||||
ambiguousLegacyJobIdx2,
|
||||
ambiguousLegacyJobIdx3,
|
||||
ambiguousLegacyJobIdx4,
|
||||
ambiguousLegacyJobIdx5,
|
||||
}
|
||||
targetAmbiguousLegacyJob := ambiguousLegacyJobs[int(ambiguousIDJob.ID)]
|
||||
|
||||
insertBeansWithExplicitIDs(t, "action_run",
|
||||
smallIDRun, otherSmallRun, normalRun, ambiguousIDRun, ambiguousLegacyRun, collisionRun, jobSmallerThanRunRun,
|
||||
)
|
||||
insertBeansWithExplicitIDs(t, "action_run_job",
|
||||
smallIDJob, otherSmallJob, normalRunJob, ambiguousIDJob, collisionJobIdx0, collisionJobIdx1,
|
||||
ambiguousLegacyJobIdx0, ambiguousLegacyJobIdx1, ambiguousLegacyJobIdx2, ambiguousLegacyJobIdx3, ambiguousLegacyJobIdx4, ambiguousLegacyJobIdx5,
|
||||
jobSmallerThanRunJob,
|
||||
)
|
||||
|
||||
t.Run("OnlyRunID", func(t *testing.T) {
|
||||
// ID-based URLs must be valid
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, smallIDRun.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, normalRun.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("OnlyRunIndex", func(t *testing.T) {
|
||||
// legacy run index should redirect to the ID-based URL
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, normalRun.Index))
|
||||
resp := user2Session.MakeRequest(t, req, http.StatusFound)
|
||||
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, normalRun.ID), resp.Header().Get("Location"))
|
||||
|
||||
// Best-effort compatibility prefers the run ID when the same number also exists as a legacy run index.
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, collisionRun.Index))
|
||||
resp = user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), fmt.Sprintf(`data-run-id="%d"`, normalRun.ID)) // because collisionRun.Index == normalRun.ID
|
||||
|
||||
// by_index=1 should force the summary page to use the legacy run index interpretation.
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d?by_index=1", user2.Name, repo.Name, collisionRun.Index))
|
||||
resp = user2Session.MakeRequest(t, req, http.StatusFound)
|
||||
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, collisionRun.ID), resp.Header().Get("Location"))
|
||||
})
|
||||
|
||||
t.Run("RunIDAndJobID", func(t *testing.T) {
|
||||
// ID-based URLs must be valid
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, smallIDRun.ID, smallIDJob.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, normalRun.ID, normalRunJob.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
// URL must resolve even when job_id < run_id.
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, jobSmallerThanRunRun.ID, jobSmallerThanRunJob.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("RunIndexAndJobIndex", func(t *testing.T) {
|
||||
// /user2/repo2/actions/runs/3/jobs/4 is ambiguous:
|
||||
// - it may resolve as the ID-based URL for run_id=3/job_id=4,
|
||||
// - or as the legacy index-based URL for run_index=3/job_index=4 which should redirect to run_id=1501/job_id=1605.
|
||||
idBasedURL := fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, ambiguousIDRun.ID, ambiguousIDJob.ID)
|
||||
indexBasedURL := fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, ambiguousLegacyRun.Index, 4) // for ambiguousLegacyJobIdx4
|
||||
assert.Equal(t, idBasedURL, indexBasedURL)
|
||||
// When both interpretations are valid, prefer the ID-based target by default.
|
||||
req := NewRequest(t, "GET", indexBasedURL)
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
redirectURL := fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, ambiguousLegacyRun.ID, targetAmbiguousLegacyJob.ID)
|
||||
// by_index=1 should explicitly force the legacy run/job index interpretation.
|
||||
req = NewRequest(t, "GET", indexBasedURL+"?by_index=1")
|
||||
resp := user2Session.MakeRequest(t, req, http.StatusFound)
|
||||
assert.Equal(t, redirectURL, resp.Header().Get("Location"))
|
||||
|
||||
// legacy job index 0 should redirect to the first job's ID
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/0", user2.Name, repo.Name, collisionRun.Index))
|
||||
resp = user2Session.MakeRequest(t, req, http.StatusFound)
|
||||
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, collisionRun.ID, collisionJobIdx0.ID), resp.Header().Get("Location"))
|
||||
|
||||
// legacy job index 1 should redirect to the second job's ID
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/1", user2.Name, repo.Name, collisionRun.Index))
|
||||
resp = user2Session.MakeRequest(t, req, http.StatusFound)
|
||||
assert.Equal(t, fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, collisionRun.ID, collisionJobIdx1.ID), resp.Header().Get("Location"))
|
||||
})
|
||||
|
||||
t.Run("InvalidURLs", func(t *testing.T) {
|
||||
// the job ID from a different run should not match
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/%d", user2.Name, repo.Name, smallIDRun.ID, otherSmallJob.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// resolve the run by index first and then return not found because the job index is out-of-range
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/2", user2.Name, repo.Name, normalRun.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// an out-of-range job index should return not found
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/2", user2.Name, repo.Name, collisionRun.Index))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// a missing run number should return not found
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d", user2.Name, repo.Name, 999999))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// a missing legacy run index should return not found
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/actions/runs/%d/jobs/0", user2.Name, repo.Name, 999999))
|
||||
user2Session.MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func insertBeansWithExplicitIDs(t *testing.T, table string, beans ...any) {
|
||||
t.Helper()
|
||||
ctx, committer, err := db.TxContext(t.Context())
|
||||
require.NoError(t, err)
|
||||
defer committer.Close()
|
||||
|
||||
if setting.Database.Type.IsMSSQL() {
|
||||
_, err = db.Exec(ctx, fmt.Sprintf("SET IDENTITY_INSERT [%s] ON", table))
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_, err = db.Exec(ctx, fmt.Sprintf("SET IDENTITY_INSERT [%s] OFF", table))
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
}
|
||||
require.NoError(t, db.Insert(ctx, beans...))
|
||||
require.NoError(t, committer.Commit())
|
||||
}
|
||||
@@ -50,30 +50,46 @@ func TestActionsRunnerModify(t *testing.T) {
|
||||
|
||||
doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, description string, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d", baseURL, id), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, sess),
|
||||
"description": description,
|
||||
})
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, sess),
|
||||
})
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id))
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
doDisable := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/update-runner?disabled=true", baseURL, id))
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
doEnable := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/update-runner?disabled=false", baseURL, id))
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
assertDenied := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
|
||||
doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusNotFound)
|
||||
doDisable(t, sess, baseURL, id, http.StatusNotFound)
|
||||
doEnable(t, sess, baseURL, id, http.StatusNotFound)
|
||||
doDelete(t, sess, baseURL, id, http.StatusNotFound)
|
||||
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
|
||||
assert.Empty(t, v.Description)
|
||||
assert.False(t, v.IsDisabled)
|
||||
}
|
||||
|
||||
assertSuccess := func(t *testing.T, sess *TestSession, baseURL string, id int64) {
|
||||
doUpdate(t, sess, baseURL, id, "ChangedDescription", http.StatusSeeOther)
|
||||
v := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
|
||||
assert.Equal(t, "ChangedDescription", v.Description)
|
||||
doDisable(t, sess, baseURL, id, http.StatusOK)
|
||||
v = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
|
||||
assert.True(t, v.IsDisabled)
|
||||
doEnable(t, sess, baseURL, id, http.StatusOK)
|
||||
v = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunner{ID: id})
|
||||
assert.False(t, v.IsDisabled)
|
||||
doDelete(t, sess, baseURL, id, http.StatusOK)
|
||||
unittest.AssertNotExistsBean(t, &actions_model.ActionRunner{ID: id})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
@@ -83,7 +84,7 @@ func (r *mockRunner) doRegister(t *testing.T, name, token string, labels []strin
|
||||
func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, runnerName string, labels []string, ephemeral bool) {
|
||||
session := loginUser(t, ownerName)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token)
|
||||
req := NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/actions/runners/registration-token", ownerName, repoName)).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var registrationToken struct {
|
||||
Token string `json:"token"`
|
||||
@@ -94,13 +95,13 @@ func (r *mockRunner) registerAsRepoRunner(t *testing.T, ownerName, repoName, run
|
||||
|
||||
func (r *mockRunner) fetchTask(t *testing.T, timeout ...time.Duration) *runnerv1.Task {
|
||||
task := r.tryFetchTask(t, timeout...)
|
||||
assert.NotNil(t, task, "failed to fetch a task")
|
||||
require.NotNil(t, task, "failed to fetch a task")
|
||||
return task
|
||||
}
|
||||
|
||||
func (r *mockRunner) fetchNoTask(t *testing.T, timeout ...time.Duration) {
|
||||
task := r.tryFetchTask(t, timeout...)
|
||||
assert.Nil(t, task, "a task is fetched")
|
||||
require.Nil(t, task, "a task is fetched")
|
||||
}
|
||||
|
||||
const defaultFetchTaskTimeout = 1 * time.Second
|
||||
@@ -113,12 +114,8 @@ func (r *mockRunner) tryFetchTask(t *testing.T, timeout ...time.Duration) *runne
|
||||
ddl := time.Now().Add(fetchTimeout)
|
||||
var task *runnerv1.Task
|
||||
for time.Now().Before(ddl) {
|
||||
resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: 0,
|
||||
}))
|
||||
assert.NoError(t, err)
|
||||
if resp.Msg.Task != nil {
|
||||
task = resp.Msg.Task
|
||||
task, _ = r.fetchTaskOnce(t, 0)
|
||||
if task != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
@@ -127,6 +124,17 @@ func (r *mockRunner) tryFetchTask(t *testing.T, timeout ...time.Duration) *runne
|
||||
return task
|
||||
}
|
||||
|
||||
// fetchTaskOnce performs a single FetchTask request with the given TasksVersion
|
||||
// and returns the task (if any) and the TasksVersion from the response.
|
||||
// Used to verify the production path where the runner sends the current version.
|
||||
func (r *mockRunner) fetchTaskOnce(t *testing.T, tasksVersion int64) (*runnerv1.Task, int64) {
|
||||
resp, err := r.client.runnerServiceClient.FetchTask(t.Context(), connect.NewRequest(&runnerv1.FetchTaskRequest{
|
||||
TasksVersion: tasksVersion,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
return resp.Msg.Task, resp.Msg.TasksVersion
|
||||
}
|
||||
|
||||
type mockTaskOutcome struct {
|
||||
result runnerv1.Result
|
||||
outputs map[string]string
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/migration"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
mirror_service "code.gitea.io/gitea/services/mirror"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
@@ -89,7 +88,9 @@ jobs:
|
||||
assert.NoError(t, err)
|
||||
|
||||
// merge pull request
|
||||
testPullMerge(t, testContext.Session, repo.OwnerName, repo.Name, strconv.FormatInt(apiPull.Index, 10), mergeStyle, false)
|
||||
testPullMerge(t, testContext.Session, repo.OwnerName, repo.Name, strconv.FormatInt(apiPull.Index, 10), MergeOptions{
|
||||
Style: mergeStyle,
|
||||
})
|
||||
|
||||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
|
||||
return pull.MergedCommitID, "@every 2m"
|
||||
@@ -101,8 +102,8 @@ jobs:
|
||||
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
|
||||
// enable manual-merge
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
HasPullRequests: util.ToPointer(true),
|
||||
AllowManualMerge: util.ToPointer(true),
|
||||
HasPullRequests: new(true),
|
||||
AllowManualMerge: new(true),
|
||||
})(t)
|
||||
|
||||
// update workflow file
|
||||
@@ -168,7 +169,7 @@ func testScheduleUpdateMirrorSync(t *testing.T) {
|
||||
// enable actions unit for mirror repo
|
||||
assert.False(t, mirrorRepo.UnitEnabled(t.Context(), unit_model.TypeActions))
|
||||
doAPIEditRepository(mirrorContext, &api.EditRepoOption{
|
||||
HasActions: util.ToPointer(true),
|
||||
HasActions: new(true),
|
||||
})(t)
|
||||
actionSchedule := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionSchedule{RepoID: mirrorRepo.ID})
|
||||
scheduleSpec := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionScheduleSpec{RepoID: mirrorRepo.ID, ScheduleID: actionSchedule.ID})
|
||||
@@ -199,11 +200,11 @@ func testScheduleUpdateMirrorSync(t *testing.T) {
|
||||
func testScheduleUpdateArchiveAndUnarchive(t *testing.T) {
|
||||
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
Archived: util.ToPointer(true),
|
||||
Archived: new(true),
|
||||
})(t)
|
||||
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
Archived: util.ToPointer(false),
|
||||
Archived: new(false),
|
||||
})(t)
|
||||
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
@@ -214,11 +215,11 @@ func testScheduleUpdateArchiveAndUnarchive(t *testing.T) {
|
||||
func testScheduleUpdateDisableAndEnableActionsUnit(t *testing.T) {
|
||||
doTestScheduleUpdate(t, func(t *testing.T, u *url.URL, testContext APITestContext, user *user_model.User, repo *repo_model.Repository) (commitID, expectedSpec string) {
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
HasActions: util.ToPointer(false),
|
||||
HasActions: new(false),
|
||||
})(t)
|
||||
assert.Zero(t, unittest.GetCount(t, &actions_model.ActionSchedule{RepoID: repo.ID}))
|
||||
doAPIEditRepository(testContext, &api.EditRepoOption{
|
||||
HasActions: util.ToPointer(true),
|
||||
HasActions: new(true),
|
||||
})(t)
|
||||
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestActionsCollaborativeOwner(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
// user2 is the owner of the private "reusable_workflow" repo
|
||||
user2Session := loginUser(t, "user2")
|
||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
apiReusableWorkflowRepo := createActionsTestRepo(t, user2Token, "reusable_workflow", true)
|
||||
reusableWorkflowRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiReusableWorkflowRepo.ID})
|
||||
|
||||
// user4 is the owner of the private caller repo
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
user4Session := loginUser(t, user4.Name)
|
||||
user4Token := getTokenForLoggedInUser(t, user4Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
apiCallerRepo := createActionsTestRepo(t, user4Token, "caller_workflow", true)
|
||||
callerRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiCallerRepo.ID})
|
||||
|
||||
// create a mock runner for caller
|
||||
runner := newMockRunner()
|
||||
runner.registerAsRepoRunner(t, callerRepo.OwnerName, callerRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||
|
||||
// init the workflow for caller
|
||||
wfTreePath := ".gitea/workflows/test_collaborative_owner.yml"
|
||||
wfFileContent := `name: Test Collaborative Owner
|
||||
on: push
|
||||
jobs:
|
||||
job1:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'test collaborative owner'
|
||||
`
|
||||
opts := getWorkflowCreateFileOptions(user4, callerRepo.DefaultBranch, "create "+wfTreePath, wfFileContent)
|
||||
createWorkflowFile(t, user4Token, callerRepo.OwnerName, callerRepo.Name, wfTreePath, opts)
|
||||
|
||||
// fetch the task and get its token
|
||||
task := runner.fetchTask(t)
|
||||
taskToken := task.Secrets["GITEA_TOKEN"]
|
||||
assert.NotEmpty(t, taskToken)
|
||||
|
||||
// prepare for clone
|
||||
dstPath := t.TempDir()
|
||||
u.Path = fmt.Sprintf("%s/%s.git", "user2", "reusable_workflow")
|
||||
u.User = url.UserPassword("gitea-actions", taskToken)
|
||||
|
||||
// the git clone will fail
|
||||
doGitCloneFail(u)(t)
|
||||
|
||||
// add user10 to the list of collaborative owners
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", reusableWorkflowRepo.OwnerName, reusableWorkflowRepo.Name), map[string]string{
|
||||
"collaborative_owner": user4.Name,
|
||||
})
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// the git clone will be successful
|
||||
doGitClone(dstPath, u)(t)
|
||||
|
||||
// remove user10 from the list of collaborative owners
|
||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/delete?id=%d", reusableWorkflowRepo.OwnerName, reusableWorkflowRepo.Name, user4.ID))
|
||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// the git clone will fail
|
||||
doGitCloneFail(u)(t)
|
||||
})
|
||||
}
|
||||
@@ -23,14 +23,12 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
actions_module "code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/commitstatus"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
@@ -40,6 +38,7 @@ import (
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPullRequestTargetEvent(t *testing.T) {
|
||||
@@ -413,7 +412,7 @@ jobs:
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create a branch
|
||||
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, gitRepo, branch.CommitID, "test-create-branch")
|
||||
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-create-branch")
|
||||
assert.NoError(t, err)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||
Title: "add workflow",
|
||||
@@ -439,7 +438,7 @@ jobs:
|
||||
assert.NotNil(t, run)
|
||||
|
||||
// delete the branch
|
||||
err = repo_service.DeleteBranch(t.Context(), user2, repo, gitRepo, "test-create-branch", nil)
|
||||
err = repo_service.DeleteBranch(t.Context(), user2, repo, gitRepo, "test-create-branch")
|
||||
assert.NoError(t, err)
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||
Title: "add workflow",
|
||||
@@ -531,9 +530,7 @@ jobs:
|
||||
|
||||
// create a new branch
|
||||
testBranch := "test-branch"
|
||||
gitRepo, err := git.OpenRepository(t.Context(), ".")
|
||||
assert.NoError(t, err)
|
||||
err = repo_service.CreateNewBranch(t.Context(), user2, repo, gitRepo, "main", testBranch)
|
||||
err = repo_service.CreateNewBranch(t.Context(), user2, repo, "main", testBranch)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create Pull
|
||||
@@ -695,6 +692,144 @@ func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha, targetURL,
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPullRequestReviewCommitStatusEvent(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // reviewer
|
||||
|
||||
// create a repo
|
||||
repo, err := repo_service.CreateRepository(t.Context(), user2, user2, repo_service.CreateRepoOptions{
|
||||
Name: "repo-pull-request-review",
|
||||
Description: "test pull-request-review commit status",
|
||||
AutoInit: true,
|
||||
Gitignores: "Go",
|
||||
License: "MIT",
|
||||
Readme: "Default",
|
||||
DefaultBranch: "main",
|
||||
IsPrivate: false,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, repo)
|
||||
|
||||
// add user4 as collaborator so they can review
|
||||
ctx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||
t.Run("AddUser4AsCollaboratorWithWriteAccess", doAPIAddCollaborator(ctx, "user4", perm.AccessModeWrite))
|
||||
|
||||
// add workflow file that triggers on pull_request_review
|
||||
addWorkflow, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: ".gitea/workflows/pr-review.yml",
|
||||
ContentReader: strings.NewReader(`name: test
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo helloworld
|
||||
`),
|
||||
},
|
||||
},
|
||||
Message: "add workflow",
|
||||
OldBranch: "main",
|
||||
NewBranch: "main",
|
||||
Author: &files_service.IdentityOptions{
|
||||
GitUserName: user2.Name,
|
||||
GitUserEmail: user2.Email,
|
||||
},
|
||||
Committer: &files_service.IdentityOptions{
|
||||
GitUserName: user2.Name,
|
||||
GitUserEmail: user2.Email,
|
||||
},
|
||||
Dates: &files_service.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, addWorkflow)
|
||||
|
||||
// create a branch and a PR
|
||||
testBranch := "test-review-branch"
|
||||
err = repo_service.CreateNewBranch(t.Context(), user2, repo, "main", testBranch)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// add a file on the test branch so the PR has changes
|
||||
addFileResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{
|
||||
{
|
||||
Operation: "create",
|
||||
TreePath: "test.txt",
|
||||
ContentReader: strings.NewReader("test content"),
|
||||
},
|
||||
},
|
||||
Message: "add test file",
|
||||
OldBranch: testBranch,
|
||||
NewBranch: testBranch,
|
||||
Author: &files_service.IdentityOptions{
|
||||
GitUserName: user2.Name,
|
||||
GitUserEmail: user2.Email,
|
||||
},
|
||||
Committer: &files_service.IdentityOptions{
|
||||
GitUserName: user2.Name,
|
||||
GitUserEmail: user2.Email,
|
||||
},
|
||||
Dates: &files_service.CommitDateOptions{
|
||||
Author: time.Now(),
|
||||
Committer: time.Now(),
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, addFileResp)
|
||||
sha := addFileResp.Commit.SHA
|
||||
|
||||
pullIssue := &issues_model.Issue{
|
||||
RepoID: repo.ID,
|
||||
Title: "A test PR for review",
|
||||
PosterID: user2.ID,
|
||||
Poster: user2,
|
||||
IsPull: true,
|
||||
}
|
||||
pullRequest := &issues_model.PullRequest{
|
||||
HeadRepoID: repo.ID,
|
||||
BaseRepoID: repo.ID,
|
||||
HeadBranch: testBranch,
|
||||
BaseBranch: "main",
|
||||
HeadRepo: repo,
|
||||
BaseRepo: repo,
|
||||
Type: issues_model.PullRequestGitea,
|
||||
}
|
||||
prOpts := &pull_service.NewPullRequestOptions{Repo: repo, Issue: pullIssue, PullRequest: pullRequest}
|
||||
err = pull_service.NewPullRequest(t.Context(), prOpts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// submit an approval review as user4
|
||||
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
_, _, err = pull_service.SubmitReview(t.Context(), user4, gitRepo, pullIssue, issues_model.ReviewTypeApprove, "lgtm", sha, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// verify that a commit status was created for the review event
|
||||
assert.Eventually(t, func() bool {
|
||||
latestCommitStatuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, sha, db.ListOptionsAll)
|
||||
assert.NoError(t, err)
|
||||
if len(latestCommitStatuses) == 0 {
|
||||
return false
|
||||
}
|
||||
if latestCommitStatuses[0].State == commitstatus.CommitStatusPending {
|
||||
insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}, 1*time.Second, 100*time.Millisecond)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWorkflowDispatchPublicApi(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
@@ -772,6 +907,27 @@ jobs:
|
||||
CommitSHA: branch.CommitID,
|
||||
})
|
||||
assert.NotNil(t, run)
|
||||
|
||||
// Now trigger with rundetails
|
||||
values.Set("return_run_details", "true")
|
||||
|
||||
req = NewRequestWithURLValues(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/workflows/dispatch.yml/dispatches", repo.FullName()), values).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
runDetails := &api.RunDetails{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(runDetails))
|
||||
assert.NotEqual(t, 0, runDetails.WorkflowRunID)
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||
ID: runDetails.WorkflowRunID,
|
||||
Title: "add workflow",
|
||||
RepoID: repo.ID,
|
||||
Event: "workflow_dispatch",
|
||||
Ref: "refs/heads/main",
|
||||
WorkflowID: "dispatch.yml",
|
||||
CommitSHA: branch.CommitID,
|
||||
})
|
||||
assert.NotNil(t, run)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1409,7 +1565,7 @@ jobs:
|
||||
// user4 forks the repo
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/forks", baseRepo.OwnerName, baseRepo.Name),
|
||||
&api.CreateForkOption{
|
||||
Name: util.ToPointer("close-pull-request-with-path-fork"),
|
||||
Name: new("close-pull-request-with-path-fork"),
|
||||
}).AddTokenAuth(user4Token)
|
||||
resp := MakeRequest(t, req, http.StatusAccepted)
|
||||
var apiForkRepo api.Repository
|
||||
@@ -1508,14 +1664,11 @@ jobs:
|
||||
assert.NotEmpty(t, addWorkflowToBaseResp)
|
||||
|
||||
// Get the commit ID of the default branch
|
||||
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create a branch
|
||||
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, gitRepo, branch.CommitID, "test-action-run-name-with-variables")
|
||||
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-action-run-name-with-variables")
|
||||
assert.NoError(t, err)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||
Title: user2.LoginName + " is running this workflow",
|
||||
@@ -1585,14 +1738,11 @@ jobs:
|
||||
assert.NotEmpty(t, addWorkflowToBaseResp)
|
||||
|
||||
// Get the commit ID of the default branch
|
||||
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
|
||||
assert.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
branch, err := git_model.GetBranch(t.Context(), repo.ID, repo.DefaultBranch)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// create a branch
|
||||
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, gitRepo, branch.CommitID, "test-action-run-name")
|
||||
err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-action-run-name")
|
||||
assert.NoError(t, err)
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{
|
||||
Title: "run name without variables",
|
||||
@@ -1623,7 +1773,7 @@ func TestPullRequestWithPathsRebase(t *testing.T) {
|
||||
testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", "dir1/dir1.txt", "1")
|
||||
testCreateFile(t, session, "user2", repoName, repo.DefaultBranch, "", "dir2/dir2.txt", "2")
|
||||
wfFileContent := `name: ci
|
||||
on:
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'dir1/**'
|
||||
@@ -1648,12 +1798,10 @@ jobs:
|
||||
apiPull, err := doAPICreatePullRequest(apiCtx, "user2", repoName, repo.DefaultBranch, "update-dir2")(t)
|
||||
runner.fetchNoTask(t)
|
||||
assert.NoError(t, err)
|
||||
testEditFile(t, session, "user2", repoName, repo.DefaultBranch, "dir1/dir1.txt", "11") // change the file in "dir1"
|
||||
req := NewRequestWithValues(t, "POST",
|
||||
fmt.Sprintf("/%s/%s/pulls/%d/update?style=rebase", "user2", repoName, apiPull.Index), // update by rebase
|
||||
map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
})
|
||||
// change the file in "dir1"
|
||||
testEditFile(t, session, "user2", repoName, repo.DefaultBranch, "dir1/dir1.txt", "11")
|
||||
// update by rebase
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/pulls/%d/update?style=rebase", "user2", repoName, apiPull.Index))
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
runner.fetchNoTask(t)
|
||||
})
|
||||
|
||||
@@ -50,17 +50,14 @@ func TestActionsVariables(t *testing.T) {
|
||||
|
||||
doUpdate := func(t *testing.T, sess *TestSession, baseURL string, id int64, data string, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", baseURL, id), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, sess),
|
||||
"name": "VAR",
|
||||
"data": data,
|
||||
"name": "VAR",
|
||||
"data": data,
|
||||
})
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
doDelete := func(t *testing.T, sess *TestSession, baseURL string, id int64, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id), map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, sess),
|
||||
})
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("%s/%d/delete", baseURL, id))
|
||||
sess.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,14 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/system"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/setting/config"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAdminConfig(t *testing.T) {
|
||||
@@ -20,4 +24,46 @@ func TestAdminConfig(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/-/admin/config")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
|
||||
|
||||
t.Run("OpenEditorWithApps", func(t *testing.T) {
|
||||
cfg := setting.Config().Repository.OpenWithEditorApps
|
||||
editorApps := cfg.Value(t.Context())
|
||||
assert.Len(t, editorApps, 3)
|
||||
assert.False(t, cfg.HasValue(t.Context()))
|
||||
|
||||
require.NoError(t, system.SetSettings(t.Context(), map[string]string{cfg.DynKey(): "[]"}))
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
|
||||
editorApps = cfg.Value(t.Context())
|
||||
assert.Len(t, editorApps, 3)
|
||||
assert.False(t, cfg.HasValue(t.Context()))
|
||||
|
||||
require.NoError(t, system.SetSettings(t.Context(), map[string]string{cfg.DynKey(): "[{}]"}))
|
||||
config.GetDynGetter().InvalidateCache()
|
||||
|
||||
editorApps = cfg.Value(t.Context())
|
||||
assert.Len(t, editorApps, 1)
|
||||
assert.True(t, cfg.HasValue(t.Context()))
|
||||
})
|
||||
|
||||
t.Run("InstanceWebBanner", func(t *testing.T) {
|
||||
banner, rev1, has := setting.Config().Instance.WebBanner.ValueRevision(t.Context())
|
||||
assert.False(t, has)
|
||||
assert.Equal(t, setting.WebBannerType{}, banner)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/-/admin/config", map[string]string{
|
||||
"key": "instance.web_banner",
|
||||
"value": `{"DisplayEnabled":true,"ContentMessage":"test-msg","StartTimeUnix":123,"EndTimeUnix":456}`,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
banner, rev2, has := setting.Config().Instance.WebBanner.ValueRevision(t.Context())
|
||||
assert.NotEqual(t, rev1, rev2)
|
||||
assert.True(t, has)
|
||||
assert.Equal(t, setting.WebBannerType{
|
||||
DisplayEnabled: true,
|
||||
ContentMessage: "test-msg",
|
||||
StartTimeUnix: 123,
|
||||
EndTimeUnix: 456,
|
||||
}, banner)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -43,18 +43,16 @@ func TestAdminViewUser(t *testing.T) {
|
||||
func TestAdminEditUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
testSuccessfullEdit(t, user_model.User{ID: 2, Name: "newusername", LoginName: "otherlogin", Email: "new@e-mail.gitea"})
|
||||
testSuccessfulEdit(t, user_model.User{ID: 2, Name: "newusername", LoginName: "otherlogin", Email: "new@e-mail.gitea"})
|
||||
}
|
||||
|
||||
func testSuccessfullEdit(t *testing.T, formData user_model.User) {
|
||||
func testSuccessfulEdit(t *testing.T, formData user_model.User) {
|
||||
makeRequest(t, formData, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func makeRequest(t *testing.T, formData user_model.User, headerCode int) {
|
||||
session := loginUser(t, "user1")
|
||||
csrf := GetUserCSRFToken(t, session)
|
||||
req := NewRequestWithValues(t, "POST", "/-/admin/users/"+strconv.Itoa(int(formData.ID))+"/edit", map[string]string{
|
||||
"_csrf": csrf,
|
||||
"user_name": formData.Name,
|
||||
"login_name": formData.LoginName,
|
||||
"login_type": "0-0",
|
||||
@@ -96,10 +94,7 @@ func TestAdminDeleteUser(t *testing.T) {
|
||||
query = "?purge=true"
|
||||
}
|
||||
|
||||
csrf := GetUserCSRFToken(t, session)
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/-/admin/users/%d/delete%s", entry.userID, query), map[string]string{
|
||||
"_csrf": csrf,
|
||||
})
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/-/admin/users/%d/delete%s", entry.userID, query))
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
assertUserDeleted(t, entry.userID)
|
||||
|
||||
@@ -6,26 +6,33 @@ package integration
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/actions"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
@@ -45,45 +52,168 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) {
|
||||
token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// acquire artifact upload url
|
||||
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
|
||||
Version: 4,
|
||||
Name: "artifact",
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
})).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var uploadResp actions.CreateArtifactResponse
|
||||
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
|
||||
assert.True(t, uploadResp.Ok)
|
||||
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
|
||||
table := []struct {
|
||||
name string
|
||||
version int32
|
||||
contentType string
|
||||
blockID bool
|
||||
noLength bool
|
||||
append int
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "artifact",
|
||||
version: 4,
|
||||
path: "artifact.zip",
|
||||
},
|
||||
{
|
||||
name: "artifact2",
|
||||
version: 4,
|
||||
blockID: true,
|
||||
},
|
||||
{
|
||||
name: "artifact3",
|
||||
version: 4,
|
||||
noLength: true,
|
||||
},
|
||||
{
|
||||
name: "artifact4",
|
||||
version: 4,
|
||||
blockID: true,
|
||||
noLength: true,
|
||||
},
|
||||
{
|
||||
name: "artifact5",
|
||||
version: 7,
|
||||
blockID: true,
|
||||
},
|
||||
{
|
||||
name: "artifact6",
|
||||
version: 7,
|
||||
append: 2,
|
||||
noLength: true,
|
||||
},
|
||||
{
|
||||
name: "artifact7",
|
||||
version: 7,
|
||||
append: 3,
|
||||
blockID: true,
|
||||
noLength: true,
|
||||
},
|
||||
{
|
||||
name: "artifact8",
|
||||
version: 7,
|
||||
append: 4,
|
||||
blockID: true,
|
||||
},
|
||||
{
|
||||
name: "artifact9.json",
|
||||
version: 7,
|
||||
contentType: "application/json",
|
||||
},
|
||||
{
|
||||
name: "artifact10",
|
||||
version: 7,
|
||||
contentType: "application/zip",
|
||||
path: "artifact10.zip",
|
||||
},
|
||||
{
|
||||
name: "artifact11.zip",
|
||||
version: 7,
|
||||
contentType: "application/zip",
|
||||
path: "artifact11.zip",
|
||||
},
|
||||
}
|
||||
|
||||
// get upload url
|
||||
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
|
||||
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
|
||||
for _, entry := range table {
|
||||
t.Run(entry.name, func(t *testing.T) {
|
||||
// acquire artifact upload url
|
||||
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
|
||||
Version: entry.version,
|
||||
Name: entry.name,
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil),
|
||||
})).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var uploadResp actions.CreateArtifactResponse
|
||||
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
|
||||
assert.True(t, uploadResp.Ok)
|
||||
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
|
||||
|
||||
// upload artifact chunk
|
||||
body := strings.Repeat("A", 1024)
|
||||
req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body))
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
h := sha256.New()
|
||||
|
||||
t.Logf("Create artifact confirm")
|
||||
blocks := make([]string, 0, util.Iif(entry.blockID, entry.append+1, 0))
|
||||
|
||||
sha := sha256.Sum256([]byte(body))
|
||||
// get upload url
|
||||
for i := range entry.append + 1 {
|
||||
url := uploadResp.SignedUploadUrl
|
||||
// See https://learn.microsoft.com/en-us/rest/api/storageservices/append-block
|
||||
// See https://learn.microsoft.com/en-us/rest/api/storageservices/put-block
|
||||
if entry.blockID {
|
||||
blockID := base64.RawURLEncoding.EncodeToString(fmt.Append([]byte("SOME_BIG_BLOCK_ID_"), i))
|
||||
blocks = append(blocks, blockID)
|
||||
url += "&comp=block&blockid=" + blockID
|
||||
} else {
|
||||
url += "&comp=appendBlock"
|
||||
}
|
||||
|
||||
// confirm artifact upload
|
||||
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
|
||||
Name: "artifact",
|
||||
Size: 1024,
|
||||
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
})).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
var finalizeResp actions.FinalizeArtifactResponse
|
||||
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
|
||||
assert.True(t, finalizeResp.Ok)
|
||||
// upload artifact chunk
|
||||
body := strings.Repeat("A", 1024)
|
||||
_, _ = h.Write([]byte(body))
|
||||
var bodyReader io.Reader = strings.NewReader(body)
|
||||
if entry.noLength {
|
||||
bodyReader = io.MultiReader(bodyReader)
|
||||
}
|
||||
req = NewRequestWithBody(t, "PUT", url, bodyReader)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
|
||||
if entry.blockID && entry.append > 0 {
|
||||
// https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list
|
||||
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
|
||||
// upload artifact blockList
|
||||
blockList := &actions.BlockList{
|
||||
Latest: blocks,
|
||||
}
|
||||
rawBlockList, err := xml.Marshal(blockList)
|
||||
assert.NoError(t, err)
|
||||
req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList))
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
|
||||
sha := h.Sum(nil)
|
||||
|
||||
t.Logf("Create artifact confirm")
|
||||
|
||||
// confirm artifact upload
|
||||
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
|
||||
Name: entry.name,
|
||||
Size: int64(entry.append+1) * 1024,
|
||||
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha)),
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
})).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
var finalizeResp actions.FinalizeArtifactResponse
|
||||
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
|
||||
assert.True(t, finalizeResp.Ok)
|
||||
|
||||
artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId})
|
||||
if entry.contentType != "" {
|
||||
assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType)
|
||||
} else {
|
||||
assert.Equal(t, "application/zip", artifact.ContentEncodingOrType)
|
||||
}
|
||||
if entry.path != "" {
|
||||
assert.Equal(t, entry.path, artifact.ArtifactPath)
|
||||
}
|
||||
assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status)
|
||||
assert.Equal(t, int64(entry.append+1)*1024, artifact.FileSize)
|
||||
assert.Equal(t, int64(entry.append+1)*1024, artifact.FileCompressedSize)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) {
|
||||
@@ -106,8 +236,7 @@ func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) {
|
||||
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
|
||||
|
||||
// get upload url
|
||||
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
|
||||
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
|
||||
url := uploadResp.SignedUploadUrl + "&comp=block"
|
||||
|
||||
// upload artifact chunk
|
||||
body := strings.Repeat("B", 1024)
|
||||
@@ -151,8 +280,7 @@ func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) {
|
||||
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
|
||||
|
||||
// get upload url
|
||||
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
|
||||
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
|
||||
url := uploadResp.SignedUploadUrl + "&comp=block"
|
||||
|
||||
// upload artifact chunk
|
||||
body := strings.Repeat("A", 1024)
|
||||
@@ -198,9 +326,8 @@ func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing
|
||||
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
|
||||
|
||||
// get upload urls
|
||||
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
|
||||
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=%2f..%2fmyfile"
|
||||
blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist"
|
||||
url := uploadResp.SignedUploadUrl + "&comp=block&blockid=%2f..%2fmyfile"
|
||||
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
|
||||
|
||||
// upload artifact chunk
|
||||
body := strings.Repeat("A", 1024)
|
||||
@@ -247,63 +374,126 @@ func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) {
|
||||
token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// acquire artifact upload url
|
||||
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
|
||||
Version: 4,
|
||||
Name: "artifactWithChunksOutOfOrder",
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
})).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var uploadResp actions.CreateArtifactResponse
|
||||
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
|
||||
assert.True(t, uploadResp.Ok)
|
||||
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
|
||||
|
||||
// get upload urls
|
||||
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
|
||||
block1URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block1"
|
||||
block2URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block2"
|
||||
blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist"
|
||||
|
||||
// upload artifact chunks
|
||||
bodyb := strings.Repeat("B", 1024)
|
||||
req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb))
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
bodya := strings.Repeat("A", 1024)
|
||||
req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya))
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// upload artifact blockList
|
||||
blockList := &actions.BlockList{
|
||||
Latest: []string{
|
||||
"block1",
|
||||
"block2",
|
||||
},
|
||||
table := []struct {
|
||||
name string
|
||||
artifactName string
|
||||
serveDirect bool
|
||||
contentType string
|
||||
}{
|
||||
{name: "Upload-Zip", artifactName: "artifact-v4-upload", contentType: ""},
|
||||
{name: "Upload-Pdf", artifactName: "report-upload.pdf", contentType: "application/pdf"},
|
||||
{name: "Upload-Html", artifactName: "report-upload.html", contentType: "application/html"},
|
||||
{name: "ServeDirect-Zip", artifactName: "artifact-v4-upload-serve-direct", contentType: "", serveDirect: true},
|
||||
{name: "ServeDirect-Pdf", artifactName: "report-upload-serve-direct.pdf", contentType: "application/pdf", serveDirect: true},
|
||||
{name: "ServeDirect-Html", artifactName: "report-upload-serve-direct.html", contentType: "application/html", serveDirect: true},
|
||||
}
|
||||
rawBlockList, err := xml.Marshal(blockList)
|
||||
assert.NoError(t, err)
|
||||
req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList))
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
t.Logf("Create artifact confirm")
|
||||
for _, entry := range table {
|
||||
t.Run(entry.name, func(t *testing.T) {
|
||||
// Only AzureBlobStorageType supports ServeDirect Uploads
|
||||
switch setting.Actions.ArtifactStorage.Type {
|
||||
case setting.AzureBlobStorageType:
|
||||
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.serveDirect)()
|
||||
default:
|
||||
if entry.serveDirect {
|
||||
t.Skip()
|
||||
}
|
||||
}
|
||||
// acquire artifact upload url
|
||||
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
|
||||
Version: util.Iif[int32](entry.contentType != "", 7, 4),
|
||||
Name: entry.artifactName,
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil),
|
||||
})).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var uploadResp actions.CreateArtifactResponse
|
||||
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
|
||||
assert.True(t, uploadResp.Ok)
|
||||
if !entry.serveDirect {
|
||||
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
|
||||
}
|
||||
|
||||
sha := sha256.Sum256([]byte(bodya + bodyb))
|
||||
// get upload urls
|
||||
block1URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block1"))
|
||||
block2URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block2"))
|
||||
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
|
||||
|
||||
// confirm artifact upload
|
||||
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
|
||||
Name: "artifactWithChunksOutOfOrder",
|
||||
Size: 2048,
|
||||
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
})).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
var finalizeResp actions.FinalizeArtifactResponse
|
||||
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
|
||||
assert.True(t, finalizeResp.Ok)
|
||||
// upload artifact chunks
|
||||
bodyb := strings.Repeat("B", 1024)
|
||||
req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb))
|
||||
if entry.serveDirect {
|
||||
req.Request.RequestURI = ""
|
||||
nresp, err := http.DefaultClient.Do(req.Request)
|
||||
require.NoError(t, err)
|
||||
nresp.Body.Close()
|
||||
require.Equal(t, http.StatusCreated, nresp.StatusCode)
|
||||
} else {
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
|
||||
bodya := strings.Repeat("A", 1024)
|
||||
req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya))
|
||||
if entry.serveDirect {
|
||||
req.Request.RequestURI = ""
|
||||
nresp, err := http.DefaultClient.Do(req.Request)
|
||||
require.NoError(t, err)
|
||||
nresp.Body.Close()
|
||||
require.Equal(t, http.StatusCreated, nresp.StatusCode)
|
||||
} else {
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
|
||||
// upload artifact blockList
|
||||
blockList := &actions.BlockList{
|
||||
Latest: []string{
|
||||
base64.RawURLEncoding.EncodeToString([]byte("block1")),
|
||||
base64.RawURLEncoding.EncodeToString([]byte("block2")),
|
||||
},
|
||||
}
|
||||
rawBlockList, err := xml.Marshal(blockList)
|
||||
assert.NoError(t, err)
|
||||
req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList))
|
||||
if entry.serveDirect {
|
||||
req.Request.RequestURI = ""
|
||||
nresp, err := http.DefaultClient.Do(req.Request)
|
||||
require.NoError(t, err)
|
||||
nresp.Body.Close()
|
||||
require.Equal(t, http.StatusCreated, nresp.StatusCode)
|
||||
} else {
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
|
||||
t.Logf("Create artifact confirm")
|
||||
|
||||
sha := sha256.Sum256([]byte(bodya + bodyb))
|
||||
|
||||
// confirm artifact upload
|
||||
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
|
||||
Name: entry.artifactName,
|
||||
Size: 2048,
|
||||
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
})).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
var finalizeResp actions.FinalizeArtifactResponse
|
||||
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
|
||||
assert.True(t, finalizeResp.Ok)
|
||||
|
||||
artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId})
|
||||
if entry.contentType != "" {
|
||||
assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType)
|
||||
} else {
|
||||
assert.Equal(t, "application/zip", artifact.ContentEncodingOrType)
|
||||
}
|
||||
assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status)
|
||||
assert.Equal(t, int64(2048), artifact.FileSize)
|
||||
assert.Equal(t, int64(2048), artifact.FileCompressedSize)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsArtifactV4DownloadSingle(t *testing.T) {
|
||||
@@ -312,33 +502,97 @@ func TestActionsArtifactV4DownloadSingle(t *testing.T) {
|
||||
token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// acquire artifact upload url
|
||||
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
|
||||
NameFilter: wrapperspb.String("artifact-v4-download"),
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
})).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var listResp actions.ListArtifactsResponse
|
||||
protojson.Unmarshal(resp.Body.Bytes(), &listResp)
|
||||
assert.Len(t, listResp.Artifacts, 1)
|
||||
table := []struct {
|
||||
Name string
|
||||
ArtifactName string
|
||||
FileName string
|
||||
ServeDirect bool
|
||||
ContentType string
|
||||
ContentDisposition string
|
||||
}{
|
||||
{Name: "Download-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip"},
|
||||
{Name: "Download-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf"},
|
||||
{Name: "Download-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html"},
|
||||
{Name: "ServeDirect-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip", ServeDirect: true},
|
||||
{Name: "ServeDirect-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf", ServeDirect: true},
|
||||
{Name: "ServeDirect-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html", ServeDirect: true},
|
||||
}
|
||||
|
||||
// confirm artifact upload
|
||||
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
|
||||
Name: "artifact-v4-download",
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
})).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
var finalizeResp actions.GetSignedArtifactURLResponse
|
||||
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
|
||||
assert.NotEmpty(t, finalizeResp.SignedUrl)
|
||||
for _, entry := range table {
|
||||
t.Run(entry.Name, func(t *testing.T) {
|
||||
switch setting.Actions.ArtifactStorage.Type {
|
||||
case setting.AzureBlobStorageType:
|
||||
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.ServeDirect)()
|
||||
case setting.MinioStorageType:
|
||||
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.MinioConfig.ServeDirect, entry.ServeDirect)()
|
||||
default:
|
||||
if entry.ServeDirect {
|
||||
t.Skip()
|
||||
}
|
||||
}
|
||||
|
||||
req = NewRequest(t, "GET", finalizeResp.SignedUrl)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
body := strings.Repeat("D", 1024)
|
||||
assert.Equal(t, body, resp.Body.String())
|
||||
// list artifacts by name
|
||||
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
|
||||
NameFilter: wrapperspb.String(entry.ArtifactName),
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
})).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var listResp actions.ListArtifactsResponse
|
||||
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp))
|
||||
require.Len(t, listResp.Artifacts, 1)
|
||||
|
||||
// list artifacts by id
|
||||
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
|
||||
IdFilter: wrapperspb.Int64(listResp.Artifacts[0].DatabaseId),
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
})).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp))
|
||||
assert.Len(t, listResp.Artifacts, 1)
|
||||
|
||||
// acquire artifact download url
|
||||
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
|
||||
Name: entry.ArtifactName,
|
||||
WorkflowRunBackendId: "792",
|
||||
WorkflowJobRunBackendId: "193",
|
||||
})).
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
var finalizeResp actions.GetSignedArtifactURLResponse
|
||||
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp))
|
||||
assert.NotEmpty(t, finalizeResp.SignedUrl)
|
||||
|
||||
body := strings.Repeat("D", 1024)
|
||||
var contentDisposition string
|
||||
if entry.ServeDirect {
|
||||
externalReq, err := http.NewRequestWithContext(t.Context(), http.MethodGet, finalizeResp.SignedUrl, nil)
|
||||
require.NoError(t, err)
|
||||
externalResp, err := http.DefaultClient.Do(externalReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, externalResp.StatusCode)
|
||||
assert.Equal(t, entry.ContentType, externalResp.Header.Get("Content-Type"))
|
||||
contentDisposition = externalResp.Header.Get("Content-Disposition")
|
||||
buf := make([]byte, 1024)
|
||||
n, err := io.ReadAtLeast(externalResp.Body, buf, len(buf))
|
||||
externalResp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, len(buf), n)
|
||||
assert.Equal(t, body, string(buf))
|
||||
} else {
|
||||
req = NewRequest(t, "GET", finalizeResp.SignedUrl)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, entry.ContentType, resp.Header().Get("Content-Type"))
|
||||
contentDisposition = resp.Header().Get("Content-Disposition")
|
||||
assert.Equal(t, body, resp.Body.String())
|
||||
}
|
||||
disposition, param, err := mime.ParseMediaType(contentDisposition)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "inline", disposition)
|
||||
assert.Equal(t, entry.FileName, param["filename"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionsArtifactV4RunDownloadSinglePublicApi(t *testing.T) {
|
||||
@@ -469,7 +723,7 @@ func TestActionsArtifactV4ListAndGetPublicApi(t *testing.T) {
|
||||
for _, artifact := range listResp.Entries {
|
||||
assert.Contains(t, artifact.URL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), artifact.ID))
|
||||
assert.Contains(t, artifact.ArchiveDownloadURL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), artifact.ID))
|
||||
req = NewRequestWithBody(t, "GET", listResp.Entries[0].URL, nil).
|
||||
req = NewRequestWithBody(t, "GET", artifact.URL, nil).
|
||||
AddTokenAuth(token)
|
||||
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testActionUserSignIn(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/user").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var u api.User
|
||||
DecodeJSON(t, resp, &u)
|
||||
assert.Equal(t, "gitea-actions", u.UserName)
|
||||
}
|
||||
|
||||
func testActionUserAccessPublicRepo(t *testing.T) {
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/raw/README.md").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "file", resp.Header().Get("x-gitea-object-type"))
|
||||
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/raw/README.md").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "file", resp.Header().Get("x-gitea-object-type"))
|
||||
}
|
||||
|
||||
func testActionUserNoAccessOtherPrivateRepo(t *testing.T) {
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo2/raw/README.md").
|
||||
AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestActionUserAccessPermission(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
t.Run("ActionUserSignIn", testActionUserSignIn)
|
||||
t.Run("ActionUserAccessPublicRepo", testActionUserAccessPublicRepo)
|
||||
t.Run("ActionUserNoAccessOtherPrivateRepo", testActionUserNoAccessOtherPrivateRepo)
|
||||
}
|
||||
@@ -6,16 +6,21 @@ package integration
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIActionsGetWorkflowRun(t *testing.T) {
|
||||
@@ -26,15 +31,45 @@ func TestAPIActionsGetWorkflowRun(t *testing.T) {
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802802", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/803", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
t.Run("GetRun", func(t *testing.T) {
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802802", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/802", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/803", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("GetJobSteps", func(t *testing.T) {
|
||||
// Insert task steps for task_id 53 (job 198) so the API can return them once the backend loads them
|
||||
_, err := db.GetEngine(t.Context()).Insert(&actions_model.ActionTaskStep{
|
||||
Name: "main",
|
||||
TaskID: 53,
|
||||
Index: 0,
|
||||
RepoID: repo.ID,
|
||||
Status: actions_model.StatusSuccess,
|
||||
Started: timeutil.TimeStamp(1683636528),
|
||||
Stopped: timeutil.TimeStamp(1683636626),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs", repo.FullName())).
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var jobList api.ActionWorkflowJobsResponse
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &jobList)
|
||||
require.NoError(t, err)
|
||||
|
||||
job198Idx := slices.IndexFunc(jobList.Entries, func(job *api.ActionWorkflowJob) bool { return job.ID == 198 })
|
||||
require.NotEqual(t, -1, job198Idx, "expected to find job 198 in run 795 jobs list")
|
||||
job198 := jobList.Entries[job198Idx]
|
||||
require.NotEmpty(t, job198.Steps, "job must return at least one step when task has steps")
|
||||
assert.Equal(t, "main", job198.Steps[0].Name, "first step name")
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIActionsGetWorkflowJob(t *testing.T) {
|
||||
@@ -134,3 +169,126 @@ func testAPIActionsDeleteRunListTasks(t *testing.T, repo *repo_model.Repository,
|
||||
assert.Equal(t, expected, findTask1)
|
||||
assert.Equal(t, expected, findTask2)
|
||||
}
|
||||
|
||||
func TestAPIActionsRerunWorkflowRun(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
t.Run("NotDone", func(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/rerun", repo.FullName())).
|
||||
AddTokenAuth(writeToken)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
|
||||
AddTokenAuth(writeToken)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
var rerunResp api.ActionWorkflowRun
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &rerunResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(795), rerunResp.ID)
|
||||
assert.Equal(t, "queued", rerunResp.Status)
|
||||
assert.Equal(t, "c2d72f548424103f01ee1dc02889c1e2bff816b0", rerunResp.HeadSha)
|
||||
|
||||
run, err := actions_model.GetRunByRepoAndID(t.Context(), repo.ID, 795)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.StatusWaiting, run.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), run.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), run.Stopped)
|
||||
|
||||
job198, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 198)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job198.Status)
|
||||
assert.Equal(t, int64(0), job198.TaskID)
|
||||
|
||||
job199, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 199)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job199.Status)
|
||||
assert.Equal(t, int64(0), job199.TaskID)
|
||||
})
|
||||
|
||||
t.Run("ForbiddenWithoutWriteScope", func(t *testing.T) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/rerun", repo.FullName())).
|
||||
AddTokenAuth(readToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/999999/rerun", repo.FullName())).
|
||||
AddTokenAuth(writeToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIActionsRerunWorkflowJob(t *testing.T) {
|
||||
defer prepareTestEnvActionsArtifacts(t)()
|
||||
|
||||
t.Run("NotDone", func(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/793/jobs/194/rerun", repo.FullName())).
|
||||
AddTokenAuth(writeToken)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
writeToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
readToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/199/rerun", repo.FullName())).
|
||||
AddTokenAuth(writeToken)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
var rerunResp api.ActionWorkflowJob
|
||||
err := json.Unmarshal(resp.Body.Bytes(), &rerunResp)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(199), rerunResp.ID)
|
||||
assert.Equal(t, "queued", rerunResp.Status)
|
||||
|
||||
run, err := actions_model.GetRunByRepoAndID(t.Context(), repo.ID, 795)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.StatusWaiting, run.Status)
|
||||
|
||||
job198, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 198)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.StatusSuccess, job198.Status)
|
||||
assert.Equal(t, int64(53), job198.TaskID)
|
||||
|
||||
job199, err := actions_model.GetRunJobByRunAndID(t.Context(), 795, 199)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job199.Status)
|
||||
assert.Equal(t, int64(0), job199.TaskID)
|
||||
})
|
||||
|
||||
t.Run("ForbiddenWithoutWriteScope", func(t *testing.T) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/199/rerun", repo.FullName())).
|
||||
AddTokenAuth(readToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("NotFoundJob", func(t *testing.T) {
|
||||
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/actions/runs/795/jobs/999999/rerun", repo.FullName())).
|
||||
AddTokenAuth(writeToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,6 +24,12 @@ func TestAPIActionsRunner(t *testing.T) {
|
||||
t.Run("RepoRunner", testActionsRunnerRepo)
|
||||
}
|
||||
|
||||
func newRunnerUpdateRequest(t *testing.T, url string, disabled bool) *RequestWrapper {
|
||||
return NewRequestWithJSON(t, http.MethodPatch, url, api.EditActionRunnerOption{
|
||||
Disabled: &disabled,
|
||||
})
|
||||
}
|
||||
|
||||
func testActionsRunnerAdmin(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
adminUsername := "user1"
|
||||
@@ -45,22 +51,39 @@ func testActionsRunnerAdmin(t *testing.T) {
|
||||
require.NotEqual(t, -1, idx)
|
||||
expectedRunner := runnerList.Entries[idx]
|
||||
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Name)
|
||||
assert.False(t, expectedRunner.Disabled)
|
||||
assert.False(t, expectedRunner.Ephemeral)
|
||||
assert.Len(t, expectedRunner.Labels, 2)
|
||||
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
|
||||
assert.Equal(t, "linux", expectedRunner.Labels[1].Name)
|
||||
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
|
||||
runnerResp := MakeRequest(t, req, http.StatusOK)
|
||||
disabledRunner := api.ActionRunner{}
|
||||
DecodeJSON(t, runnerResp, &disabledRunner)
|
||||
assert.True(t, disabledRunner.Disabled)
|
||||
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/admin/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Verify all returned runners can be requested and deleted
|
||||
for _, runnerEntry := range runnerList.Entries {
|
||||
// Verify get the runner by id
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
|
||||
runnerResp := MakeRequest(t, req, http.StatusOK)
|
||||
runnerResp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
runner := api.ActionRunner{}
|
||||
DecodeJSON(t, runnerResp, &runner)
|
||||
|
||||
assert.Equal(t, runnerEntry.Name, runner.Name)
|
||||
assert.Equal(t, runnerEntry.ID, runner.ID)
|
||||
assert.Equal(t, runnerEntry.Disabled, runner.Disabled)
|
||||
assert.Equal(t, runnerEntry.Ephemeral, runner.Ephemeral)
|
||||
assert.ElementsMatch(t, runnerEntry.Labels, runner.Labels)
|
||||
|
||||
@@ -93,6 +116,7 @@ func testActionsRunnerUser(t *testing.T) {
|
||||
assert.Len(t, runnerList.Entries, 1)
|
||||
assert.Equal(t, "runner_to_be_deleted-user", runnerList.Entries[0].Name)
|
||||
assert.Equal(t, int64(34346), runnerList.Entries[0].ID)
|
||||
assert.False(t, runnerList.Entries[0].Disabled)
|
||||
assert.False(t, runnerList.Entries[0].Ephemeral)
|
||||
assert.Len(t, runnerList.Entries[0].Labels, 2)
|
||||
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
|
||||
@@ -107,11 +131,26 @@ func testActionsRunnerUser(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "runner_to_be_deleted-user", runner.Name)
|
||||
assert.Equal(t, int64(34346), runner.ID)
|
||||
assert.False(t, runner.Disabled)
|
||||
assert.False(t, runner.Ephemeral)
|
||||
assert.Len(t, runner.Labels, 2)
|
||||
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
|
||||
assert.Equal(t, "linux", runner.Labels[1].Name)
|
||||
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
||||
runnerResp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, runnerResp, &runner)
|
||||
assert.True(t, runner.Disabled)
|
||||
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Verify delete the runner by id
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
@@ -136,6 +175,7 @@ func testActionsRunnerOwner(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
|
||||
assert.Equal(t, int64(34347), runner.ID)
|
||||
assert.False(t, runner.Disabled)
|
||||
assert.False(t, runner.Ephemeral)
|
||||
assert.Len(t, runner.Labels, 2)
|
||||
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
|
||||
@@ -165,6 +205,7 @@ func testActionsRunnerOwner(t *testing.T) {
|
||||
require.NotNil(t, expectedRunner)
|
||||
assert.Equal(t, "runner_to_be_deleted-org", expectedRunner.Name)
|
||||
assert.Equal(t, int64(34347), expectedRunner.ID)
|
||||
assert.False(t, expectedRunner.Disabled)
|
||||
assert.False(t, expectedRunner.Ephemeral)
|
||||
assert.Len(t, expectedRunner.Labels, 2)
|
||||
assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
|
||||
@@ -179,11 +220,26 @@ func testActionsRunnerOwner(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
|
||||
assert.Equal(t, int64(34347), runner.ID)
|
||||
assert.False(t, runner.Disabled)
|
||||
assert.False(t, runner.Ephemeral)
|
||||
assert.Len(t, runner.Labels, 2)
|
||||
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
|
||||
assert.Equal(t, "linux", runner.Labels[1].Name)
|
||||
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
|
||||
runnerResp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, runnerResp, &runner)
|
||||
assert.True(t, runner.Disabled)
|
||||
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Verify delete the runner by id
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
@@ -202,6 +258,22 @@ func testActionsRunnerOwner(t *testing.T) {
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("DisableReadScopeForbidden", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
|
||||
|
||||
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("UpdateReadScopeForbidden", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
|
||||
|
||||
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("GetRepoScopeForbidden", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
|
||||
@@ -244,6 +316,7 @@ func testActionsRunnerRepo(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
|
||||
assert.Equal(t, int64(34348), runner.ID)
|
||||
assert.False(t, runner.Disabled)
|
||||
assert.False(t, runner.Ephemeral)
|
||||
assert.Len(t, runner.Labels, 2)
|
||||
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
|
||||
@@ -269,6 +342,7 @@ func testActionsRunnerRepo(t *testing.T) {
|
||||
assert.Len(t, runnerList.Entries, 1)
|
||||
assert.Equal(t, "runner_to_be_deleted-repo1", runnerList.Entries[0].Name)
|
||||
assert.Equal(t, int64(34348), runnerList.Entries[0].ID)
|
||||
assert.False(t, runnerList.Entries[0].Disabled)
|
||||
assert.False(t, runnerList.Entries[0].Ephemeral)
|
||||
assert.Len(t, runnerList.Entries[0].Labels, 2)
|
||||
assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
|
||||
@@ -283,11 +357,26 @@ func testActionsRunnerRepo(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
|
||||
assert.Equal(t, int64(34348), runner.ID)
|
||||
assert.False(t, runner.Disabled)
|
||||
assert.False(t, runner.Ephemeral)
|
||||
assert.Len(t, runner.Labels, 2)
|
||||
assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
|
||||
assert.Equal(t, "linux", runner.Labels[1].Name)
|
||||
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
||||
runnerResp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, runnerResp, &runner)
|
||||
assert.True(t, runner.Disabled)
|
||||
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Verify delete the runner by id
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
@@ -306,6 +395,22 @@ func testActionsRunnerRepo(t *testing.T) {
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("DisableReadScopeForbidden", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348), true).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("UpdateReadScopeForbidden", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
req := newRunnerUpdateRequest(t, fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348), false).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("GetOrganizationScopeForbidden", func(t *testing.T) {
|
||||
userUsername := "user2"
|
||||
token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/activitypub"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/routers"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestActivityPubPerson(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
t.Run("ExistingPerson", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
userID := 2
|
||||
username := "user2"
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/activitypub/user-id/%v", userID))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
body := resp.Body.Bytes()
|
||||
assert.Contains(t, string(body), "@context")
|
||||
|
||||
var person ap.Person
|
||||
err := person.UnmarshalJSON(body)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, ap.PersonType, person.Type)
|
||||
assert.Equal(t, username, person.PreferredUsername.String())
|
||||
keyID := person.GetID().String()
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v$", userID), keyID)
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v/outbox$", userID), person.Outbox.GetID().String())
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user-id/%v/inbox$", userID), person.Inbox.GetID().String())
|
||||
|
||||
pubKey := person.PublicKey
|
||||
assert.NotNil(t, pubKey)
|
||||
publicKeyID := keyID + "#main-key"
|
||||
assert.Equal(t, pubKey.ID.String(), publicKeyID)
|
||||
|
||||
pubKeyPem := pubKey.PublicKeyPem
|
||||
assert.NotNil(t, pubKeyPem)
|
||||
assert.Regexp(t, "^-----BEGIN PUBLIC KEY-----", pubKeyPem)
|
||||
})
|
||||
t.Run("MissingPerson", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/api/v1/activitypub/user-id/999999999")
|
||||
resp := MakeRequest(t, req, http.StatusNotFound)
|
||||
assert.Contains(t, resp.Body.String(), "user does not exist")
|
||||
})
|
||||
t.Run("MissingPersonInbox", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
srv := httptest.NewServer(testWebRoutes)
|
||||
defer srv.Close()
|
||||
defer test.MockVariableValue(&setting.AppURL, srv.URL+"/")()
|
||||
|
||||
username1 := "user1"
|
||||
ctx := t.Context()
|
||||
user1, err := user_model.GetUserByName(ctx, username1)
|
||||
assert.NoError(t, err)
|
||||
user1url := srv.URL + "/api/v1/activitypub/user-id/1#main-key"
|
||||
c, err := activitypub.NewClient(t.Context(), user1, user1url)
|
||||
assert.NoError(t, err)
|
||||
user2inboxurl := srv.URL + "/api/v1/activitypub/user-id/2/inbox"
|
||||
|
||||
// Signed request succeeds
|
||||
resp, err := c.Post([]byte{}, user2inboxurl)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
|
||||
// Unsigned request fails
|
||||
req := NewRequest(t, "POST", user2inboxurl)
|
||||
MakeRequest(t, req, http.StatusInternalServerError)
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -235,7 +237,40 @@ func TestAPIRenameBranch(t *testing.T) {
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
resp := testAPIRenameBranch(t, "user2", "user2", "repo1", from, "new-branch-name", http.StatusForbidden)
|
||||
assert.Contains(t, resp.Body.String(), "Branch is protected by glob-based protection rules.")
|
||||
assert.Contains(t, resp.Body.String(), "Failed to rename branch due to branch protection rules.")
|
||||
})
|
||||
t.Run("RenameBranchToMatchProtectionRulesWithAllowedUser", func(t *testing.T) {
|
||||
// allow an admin (the owner in this case) to rename a regular branch to one that matches a branch protection rule
|
||||
repoName := "repo1"
|
||||
ownerName := "user2"
|
||||
from := "regular-branch-1"
|
||||
ctx := NewAPITestContext(t, ownerName, repoName, auth_model.AccessTokenScopeWriteRepository)
|
||||
testAPICreateBranch(t, ctx.Session, ownerName, repoName, "", from, http.StatusCreated)
|
||||
|
||||
// NOTE: The protected/** branch protection rule was created in a previous test, with push enabled.
|
||||
testAPIRenameBranch(t, ownerName, ownerName, repoName, from, "protected/2", http.StatusNoContent)
|
||||
})
|
||||
t.Run("RenameBranchToMatchProtectionRulesWithUnauthorizedUser", func(t *testing.T) {
|
||||
// don't allow renaming a regular branch to a protected branch if the doer is not in the push whitelist
|
||||
repoName := "repo1"
|
||||
ownerName := "user2"
|
||||
pushWhitelist := []string{ownerName}
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/branch_protections", ownerName, repoName),
|
||||
&api.BranchProtection{
|
||||
RuleName: "owner-protected/**",
|
||||
PushWhitelistUsernames: pushWhitelist,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
from := "regular-branch-2"
|
||||
ctx := NewAPITestContext(t, ownerName, repoName, auth_model.AccessTokenScopeWriteRepository)
|
||||
testAPICreateBranch(t, ctx.Session, ownerName, repoName, "", from, http.StatusCreated)
|
||||
|
||||
unprivilegedUser := "user40"
|
||||
resp := testAPIRenameBranch(t, unprivilegedUser, ownerName, repoName, from, "owner-protected/1", http.StatusForbidden)
|
||||
|
||||
assert.Contains(t, resp.Body.String(), "Failed to rename branch due to branch protection rules.")
|
||||
})
|
||||
t.Run("RenameBranchNormalScenario", func(t *testing.T) {
|
||||
testAPIRenameBranch(t, "user2", "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent)
|
||||
@@ -243,6 +278,79 @@ func TestAPIRenameBranch(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIUpdateBranchReference(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) {
|
||||
ctx := NewAPITestContext(t, "user2", "update-branch", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
giteaURL.Path = ctx.GitPath()
|
||||
|
||||
var defaultBranch string
|
||||
t.Run("CreateRepo", doAPICreateRepository(ctx, false, func(t *testing.T, repo api.Repository) {
|
||||
defaultBranch = repo.DefaultBranch
|
||||
}))
|
||||
|
||||
createBranchReq := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/branches", ctx.Username, ctx.Reponame), &api.CreateBranchRepoOption{
|
||||
BranchName: "feature",
|
||||
OldRefName: defaultBranch,
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
ctx.Session.MakeRequest(t, createBranchReq, http.StatusCreated)
|
||||
|
||||
var featureInitialCommit string
|
||||
t.Run("LoadFeatureBranch", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
|
||||
featureInitialCommit = branch.Commit.ID
|
||||
assert.NotEmpty(t, featureInitialCommit)
|
||||
}))
|
||||
|
||||
content := base64.StdEncoding.EncodeToString([]byte("branch update test"))
|
||||
var newCommit string
|
||||
doAPICreateFile(ctx, "docs/update.txt", &api.CreateFileOptions{
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: defaultBranch,
|
||||
NewBranchName: defaultBranch,
|
||||
Message: "add docs/update.txt",
|
||||
},
|
||||
ContentBase64: content,
|
||||
}, func(t *testing.T, resp api.FileResponse) {
|
||||
newCommit = resp.Commit.SHA
|
||||
assert.NotEmpty(t, newCommit)
|
||||
})(t)
|
||||
|
||||
updateReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
|
||||
NewCommitID: newCommit,
|
||||
OldCommitID: featureInitialCommit,
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
ctx.Session.MakeRequest(t, updateReq, http.StatusNoContent)
|
||||
|
||||
t.Run("FastForwardApplied", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
|
||||
assert.Equal(t, newCommit, branch.Commit.ID)
|
||||
}))
|
||||
|
||||
staleReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
|
||||
NewCommitID: newCommit,
|
||||
OldCommitID: featureInitialCommit,
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
ctx.Session.MakeRequest(t, staleReq, http.StatusUnprocessableEntity)
|
||||
|
||||
nonFFReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
|
||||
NewCommitID: featureInitialCommit,
|
||||
OldCommitID: newCommit,
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
ctx.Session.MakeRequest(t, nonFFReq, http.StatusUnprocessableEntity)
|
||||
|
||||
forceReq := NewRequestWithJSON(t, "PUT", fmt.Sprintf("/api/v1/repos/%s/%s/branches/%s", ctx.Username, ctx.Reponame, "feature"), &api.UpdateBranchRepoOption{
|
||||
NewCommitID: featureInitialCommit,
|
||||
OldCommitID: newCommit,
|
||||
Force: true,
|
||||
}).AddTokenAuth(ctx.Token)
|
||||
ctx.Session.MakeRequest(t, forceReq, http.StatusNoContent)
|
||||
|
||||
t.Run("ForceApplied", doAPIGetBranch(ctx, "feature", func(t *testing.T, branch api.Branch) {
|
||||
assert.Equal(t, featureInitialCommit, branch.Commit.ID)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder {
|
||||
token := getUserToken(t, doerName, auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.RenameBranchRepoOption{
|
||||
@@ -254,15 +362,20 @@ func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to s
|
||||
func TestAPIBranchProtection(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// Branch protection on branch that not exist
|
||||
testAPICreateBranchProtection(t, "master/doesnotexist", 1, http.StatusCreated)
|
||||
// Can create branch protection on branch that not exist
|
||||
testAPICreateBranchProtection(t, "non-existing/branch", 1, http.StatusCreated)
|
||||
testAPIGetBranchProtection(t, "non-existing/branch", http.StatusOK)
|
||||
testAPIDeleteBranchProtection(t, "non-existing/branch", http.StatusNoContent)
|
||||
|
||||
// Get branch protection on branch that exist but not branch protection
|
||||
testAPIGetBranchProtection(t, "master", http.StatusNotFound)
|
||||
|
||||
testAPICreateBranchProtection(t, "master", 2, http.StatusCreated)
|
||||
testAPICreateBranchProtection(t, "master", 1, http.StatusCreated)
|
||||
// Can only create once
|
||||
testAPICreateBranchProtection(t, "master", 0, http.StatusForbidden)
|
||||
|
||||
testAPICreateBranchProtection(t, "other-branch", 2, http.StatusCreated)
|
||||
|
||||
// Can't delete a protected branch
|
||||
testAPIDeleteBranch(t, "master", http.StatusForbidden)
|
||||
|
||||
@@ -294,6 +407,8 @@ func TestAPIBranchProtection(t *testing.T) {
|
||||
// Test branch deletion
|
||||
testAPIDeleteBranch(t, "master", http.StatusForbidden)
|
||||
testAPIDeleteBranch(t, "branch2", http.StatusNoContent)
|
||||
testAPIDeleteBranch(t, "branch2", http.StatusNotFound) // deleted branch, there is a record in DB with IsDelete=true
|
||||
testAPIDeleteBranch(t, "no-such-branch", http.StatusNotFound) // non-existing branch, not exist in git or DB
|
||||
}
|
||||
|
||||
func TestAPICreateBranchWithSyncBranches(t *testing.T) {
|
||||
@@ -303,7 +418,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) {
|
||||
RepoID: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, branches, 6)
|
||||
assert.Len(t, branches, 8)
|
||||
|
||||
// make a broke repository with no branch on database
|
||||
_, err = db.DeleteByBean(t.Context(), git_model.Branch{RepoID: 1})
|
||||
@@ -320,7 +435,7 @@ func TestAPICreateBranchWithSyncBranches(t *testing.T) {
|
||||
RepoID: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, branches, 7)
|
||||
assert.Len(t, branches, 9)
|
||||
|
||||
branches, err = db.Find[git_model.Branch](t.Context(), git_model.FindBranchOptions{
|
||||
RepoID: 1,
|
||||
|
||||
@@ -19,15 +19,35 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateForkNoLogin(t *testing.T) {
|
||||
func TestAPIFork(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("CreateForkNoLogin", testCreateForkNoLogin)
|
||||
t.Run("CreateForkOrgNoCreatePermission", testCreateForkOrgNoCreatePermission)
|
||||
t.Run("APIForkListLimitedAndPrivateRepos", testAPIForkListLimitedAndPrivateRepos)
|
||||
t.Run("GetPrivateReposForks", testGetPrivateReposForks)
|
||||
}
|
||||
|
||||
func testCreateForkNoLogin(t *testing.T) {
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{})
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
func testCreateForkOrgNoCreatePermission(t *testing.T) {
|
||||
user4Sess := loginUser(t, "user4")
|
||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||
|
||||
canCreate, err := org_model.OrgFromUser(org).CanCreateOrgRepo(t.Context(), 4)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, canCreate)
|
||||
|
||||
user4Token := getTokenForLoggedInUser(t, user4Sess, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteOrganization)
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
|
||||
Organization: &org.Name,
|
||||
}).AddTokenAuth(user4Token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func testAPIForkListLimitedAndPrivateRepos(t *testing.T) {
|
||||
user1Sess := loginUser(t, "user1")
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user1"})
|
||||
|
||||
@@ -64,10 +84,7 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var forks []*api.Repository
|
||||
DecodeJSON(t, resp, &forks)
|
||||
|
||||
forks := DecodeJSON(t, resp, []*api.Repository{})
|
||||
assert.Empty(t, forks)
|
||||
assert.Equal(t, "0", resp.Header().Get("X-Total-Count"))
|
||||
})
|
||||
@@ -78,9 +95,7 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var forks []*api.Repository
|
||||
DecodeJSON(t, resp, &forks)
|
||||
|
||||
forks := DecodeJSON(t, resp, []*api.Repository{})
|
||||
assert.Len(t, forks, 2)
|
||||
assert.Equal(t, "2", resp.Header().Get("X-Total-Count"))
|
||||
|
||||
@@ -88,28 +103,22 @@ func TestAPIForkListLimitedAndPrivateRepos(t *testing.T) {
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(user1Token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
forks = []*api.Repository{}
|
||||
DecodeJSON(t, resp, &forks)
|
||||
|
||||
forks = DecodeJSON(t, resp, []*api.Repository{})
|
||||
assert.Len(t, forks, 2)
|
||||
assert.Equal(t, "2", resp.Header().Get("X-Total-Count"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetPrivateReposForks(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testGetPrivateReposForks(t *testing.T) {
|
||||
user1Sess := loginUser(t, "user1")
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) // private repository
|
||||
privateOrg := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23})
|
||||
user1Token := getTokenForLoggedInUser(t, user1Sess, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
forkedRepoName := "forked-repo"
|
||||
// create fork from a private repository
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+repo2.FullName()+"/forks", &api.CreateForkOption{
|
||||
Organization: &privateOrg.Name,
|
||||
Name: &forkedRepoName,
|
||||
Name: new("forked-repo"),
|
||||
}).AddTokenAuth(user1Token)
|
||||
MakeRequest(t, req, http.StatusAccepted)
|
||||
|
||||
@@ -117,8 +126,7 @@ func TestGetPrivateReposForks(t *testing.T) {
|
||||
req = NewRequest(t, "GET", "/api/v1/repos/"+repo2.FullName()+"/forks").AddTokenAuth(user1Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
forks := []*api.Repository{}
|
||||
DecodeJSON(t, resp, &forks)
|
||||
forks := DecodeJSON(t, resp, []*api.Repository{})
|
||||
assert.Len(t, forks, 1)
|
||||
assert.Equal(t, "1", resp.Header().Get("X-Total-Count"))
|
||||
assert.Equal(t, "forked-repo", forks[0].Name)
|
||||
|
||||
@@ -63,8 +63,8 @@ func TestGPGKeys(t *testing.T) {
|
||||
t.Run("CreateInvalidGPGKey", func(t *testing.T) {
|
||||
testCreateInvalidGPGKey(t, tc.makeRequest, tc.token, tc.results[4])
|
||||
})
|
||||
t.Run("CreateNoneRegistredEmailGPGKey", func(t *testing.T) {
|
||||
testCreateNoneRegistredEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[5])
|
||||
t.Run("CreateNoneRegisteredEmailGPGKey", func(t *testing.T) {
|
||||
testCreateNoneRegisteredEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[5])
|
||||
})
|
||||
t.Run("CreateValidGPGKey", func(t *testing.T) {
|
||||
testCreateValidGPGKey(t, tc.makeRequest, tc.token, tc.results[6])
|
||||
@@ -179,7 +179,7 @@ func testCreateInvalidGPGKey(t *testing.T, makeRequest makeRequestFunc, token st
|
||||
testCreateGPGKey(t, makeRequest, token, expected, "invalid_key")
|
||||
}
|
||||
|
||||
func testCreateNoneRegistredEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
|
||||
func testCreateNoneRegisteredEmailGPGKey(t *testing.T, makeRequest makeRequestFunc, token string, expected int) {
|
||||
testCreateGPGKey(t, makeRequest, token, expected, `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQENBFmGUygBCACjCNbKvMGgp0fd5vyFW9olE1CLCSyyF9gQN2hSuzmZLuAZF2Kh
|
||||
|
||||
@@ -95,9 +95,7 @@ func TestHTTPSigCert(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user1")
|
||||
|
||||
csrf := GetUserCSRFToken(t, session)
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings/keys", map[string]string{
|
||||
"_csrf": csrf,
|
||||
"content": "user1",
|
||||
"title": "principal",
|
||||
"type": "principal",
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@@ -179,3 +180,19 @@ func TestAPIRepoValidateIssueConfig(t *testing.T) {
|
||||
assert.NotEmpty(t, issueConfigValidation.Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIRepoIssueConfigRequiresCodeUnit(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
for _, path := range []string{
|
||||
fmt.Sprintf("/api/v1/repos/%s/issue_config", repo.FullName()),
|
||||
fmt.Sprintf("/api/v1/repos/%s/issue_config/validate", repo.FullName()),
|
||||
} {
|
||||
req := NewRequest(t, "GET", path).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func enableRepoDependencies(t *testing.T, repoID int64) {
|
||||
|
||||
repoUnit := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoUnit{RepoID: repoID, Type: unit.TypeIssues})
|
||||
repoUnit.IssuesConfig().EnableDependencies = true
|
||||
assert.NoError(t, repo_model.UpdateRepoUnit(t.Context(), repoUnit))
|
||||
assert.NoError(t, repo_model.UpdateRepoUnitConfig(t.Context(), repoUnit))
|
||||
}
|
||||
|
||||
func TestAPICreateIssueDependencyCrossRepoPermission(t *testing.T) {
|
||||
|
||||
@@ -44,7 +44,7 @@ func TestAPIIssuesReactions(t *testing.T) {
|
||||
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
|
||||
Reaction: "zzz",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Add allowed reaction
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
@@ -55,7 +55,10 @@ func TestAPIIssuesReactions(t *testing.T) {
|
||||
DecodeJSON(t, resp, &apiNewReaction)
|
||||
|
||||
// Add existing reaction
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "rocket",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Blocked user can't react to comment
|
||||
user34 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 34})
|
||||
@@ -111,7 +114,7 @@ func TestAPICommentReactions(t *testing.T) {
|
||||
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.EditReactionOption{
|
||||
Reaction: "eyes",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
t.Run("UnrelatedCommentID", func(t *testing.T) {
|
||||
// Using the ID of a comment that does not belong to the repository must fail
|
||||
@@ -142,7 +145,10 @@ func TestAPICommentReactions(t *testing.T) {
|
||||
DecodeJSON(t, resp, &apiNewReaction)
|
||||
|
||||
// Add existing reaction
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
req = NewRequestWithJSON(t, "POST", urlStr, &api.EditReactionOption{
|
||||
Reaction: "+1",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Get end result of reaction list of issue #1
|
||||
req = NewRequest(t, "GET", urlStr).
|
||||
|
||||
@@ -8,10 +8,12 @@ import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -53,3 +55,19 @@ about: bar
|
||||
assert.Equal(t, "error occurs when parsing issue template: count=2", resp.Header().Get("X-Gitea-Warning"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIIssueTemplateRequiresCodeUnit(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 24})
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
token := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadRepository)
|
||||
issueTemplatesURL := "/api/v1/repos/" + repo.FullName() + "/issue_templates"
|
||||
languagesURL := "/api/v1/repos/" + repo.FullName() + "/languages"
|
||||
|
||||
req := NewRequest(t, "GET", issueTemplatesURL).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
req = NewRequest(t, "GET", languagesURL).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
@@ -19,14 +19,25 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIListIssues(t *testing.T) {
|
||||
func TestAPIIssue(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("ListIssues", testAPIListIssues)
|
||||
t.Run("ListIssuesPublicOnly", testAPIListIssuesPublicOnly)
|
||||
t.Run("SearchIssues", testAPISearchIssues)
|
||||
t.Run("SearchIssuesWithLabels", testAPISearchIssuesWithLabels)
|
||||
t.Run("EditIssue", testAPIEditIssue)
|
||||
t.Run("IssueContentVersion", testAPIIssueContentVersion)
|
||||
t.Run("CreateIssue", testAPICreateIssue)
|
||||
t.Run("CreateIssueParallel", testAPICreateIssueParallel)
|
||||
}
|
||||
|
||||
func testAPIListIssues(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
@@ -74,9 +85,7 @@ func TestAPIListIssues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIListIssuesPublicOnly(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testAPIListIssuesPublicOnly(t *testing.T) {
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo1.OwnerID})
|
||||
|
||||
@@ -99,11 +108,10 @@ func TestAPIListIssuesPublicOnly(t *testing.T) {
|
||||
|
||||
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPICreateIssue(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
func testAPICreateIssue(t *testing.T) {
|
||||
const body, title = "apiTestBody", "apiTestTitle"
|
||||
|
||||
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
@@ -141,9 +149,7 @@ func TestAPICreateIssue(t *testing.T) {
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func TestAPICreateIssueParallel(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testAPICreateIssueParallel(t *testing.T) {
|
||||
// FIXME: There seems to be a bug in github.com/mattn/go-sqlite3 with sqlite_unlock_notify, when doing concurrent writes to the same database,
|
||||
// some requests may get stuck in "go-sqlite3.(*SQLiteRows).Next", "go-sqlite3.(*SQLiteStmt).exec" and "go-sqlite3.unlock_notify_wait",
|
||||
// because the "unlock_notify_wait" never returns and the internal lock never gets releases.
|
||||
@@ -151,7 +157,7 @@ func TestAPICreateIssueParallel(t *testing.T) {
|
||||
// The trigger is: a previous test created issues and made the real issue indexer queue start processing, then this test does concurrent writing.
|
||||
// Adding this "Sleep" makes go-sqlite3 "finish" some internal operations before concurrent writes and then won't get stuck.
|
||||
// To reproduce: make a new test run these 2 tests enough times:
|
||||
// > func TestBug() { for i := 0; i < 100; i++ { testAPICreateIssue(t); testAPICreateIssueParallel(t) } }
|
||||
// > func testBug() { for i := 0; i < 100; i++ { testAPICreateIssue(t); testAPICreateIssueParallel(t) } }
|
||||
// Usually the test gets stuck in fewer than 10 iterations without this "sleep".
|
||||
time.Sleep(time.Second)
|
||||
|
||||
@@ -196,9 +202,7 @@ func TestAPICreateIssueParallel(t *testing.T) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestAPIEditIssue(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testAPIEditIssue(t *testing.T) {
|
||||
issueBefore := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
|
||||
repoBefore := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issueBefore.RepoID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repoBefore.OwnerID})
|
||||
@@ -262,11 +266,9 @@ func TestAPIEditIssue(t *testing.T) {
|
||||
assert.Equal(t, title, issueAfter.Title)
|
||||
}
|
||||
|
||||
func TestAPISearchIssues(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// as this API was used in the frontend, it uses UI page size
|
||||
expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures
|
||||
func testAPISearchIssues(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.API.DefaultPagingNum, 20)()
|
||||
expectedIssueCount := 20 // 20 is from the fixtures
|
||||
|
||||
link, _ := url.Parse("/api/v1/repos/issues/search")
|
||||
token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadIssue)
|
||||
@@ -361,11 +363,37 @@ func TestAPISearchIssues(t *testing.T) {
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.Len(t, apiIssues, 2)
|
||||
|
||||
query = url.Values{"created": {"1"}} // issues created by the auth user
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.Len(t, apiIssues, 5)
|
||||
|
||||
query = url.Values{"created": {"1"}, "type": {"pulls"}} // prs created by the auth user
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.Len(t, apiIssues, 3)
|
||||
|
||||
query = url.Values{"created_by": {"user2"}} // issues created by the user2
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.Len(t, apiIssues, 9)
|
||||
|
||||
query = url.Values{"created_by": {"user2"}, "type": {"pulls"}} // prs created by user2
|
||||
link.RawQuery = query.Encode()
|
||||
req = NewRequest(t, "GET", link.String()).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.Len(t, apiIssues, 3)
|
||||
}
|
||||
|
||||
func TestAPISearchIssuesWithLabels(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testAPISearchIssuesWithLabels(t *testing.T) {
|
||||
// as this API was used in the frontend, it uses UI page size
|
||||
expectedIssueCount := min(20, setting.UI.IssuePagingNum) // 20 is from the fixtures
|
||||
|
||||
@@ -420,3 +448,55 @@ func TestAPISearchIssuesWithLabels(t *testing.T) {
|
||||
DecodeJSON(t, resp, &apiIssues)
|
||||
assert.Len(t, apiIssues, 2)
|
||||
}
|
||||
|
||||
func testAPIIssueContentVersion(t *testing.T) {
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 10})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d", owner.Name, repo.Name, issue.Index)
|
||||
|
||||
t.Run("ResponseIncludesContentVersion", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiIssue := DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.GreaterOrEqual(t, apiIssue.ContentVersion, 0)
|
||||
})
|
||||
|
||||
t.Run("EditWithCorrectVersion", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
before := DecodeJSON(t, resp, &api.Issue{})
|
||||
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
||||
Body: new("updated body with correct version"),
|
||||
ContentVersion: new(before.ContentVersion),
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
after := DecodeJSON(t, resp, &api.Issue{})
|
||||
assert.Equal(t, "updated body with correct version", after.Body)
|
||||
assert.Greater(t, after.ContentVersion, before.ContentVersion)
|
||||
})
|
||||
|
||||
t.Run("EditWithWrongVersion", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
||||
Body: new("should fail"),
|
||||
ContentVersion: new(99999),
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusConflict)
|
||||
})
|
||||
|
||||
t.Run("EditWithoutVersion", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditIssueOption{
|
||||
Body: new("edit without version succeeds"),
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,6 +79,12 @@ func TestAPIDeleteTrackedTime(t *testing.T) {
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
// Deletion should be scoped to the issue in the URL
|
||||
time5 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 5})
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time5.ID).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
time3 := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{ID: 3})
|
||||
req = NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/times/%d", user2.Name, issue2.Repo.Name, issue2.Index, time3.ID).
|
||||
AddTokenAuth(token)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/routers"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNodeinfo(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Federation.Enabled, true)()
|
||||
defer test.MockVariableValue(&testWebRoutes, routers.NormalRoutes())()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/nodeinfo")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
VerifyJSONSchema(t, resp, "nodeinfo_2.1.json")
|
||||
|
||||
var nodeinfo api.NodeInfo
|
||||
DecodeJSON(t, resp, &nodeinfo)
|
||||
assert.True(t, nodeinfo.OpenRegistrations)
|
||||
assert.Equal(t, "gitea", nodeinfo.Software.Name)
|
||||
assert.Equal(t, 29, nodeinfo.Usage.Users.Total)
|
||||
assert.Equal(t, 22, nodeinfo.Usage.LocalPosts)
|
||||
assert.Equal(t, 3, nodeinfo.Usage.LocalComments)
|
||||
}
|
||||
@@ -200,7 +200,7 @@ func TestAPINotificationPUT(t *testing.T) {
|
||||
assert.False(t, apiNL[0].Pinned)
|
||||
|
||||
//
|
||||
// Now nofication ID 2 is the first in the list and is unread.
|
||||
// Now notification ID 2 is the first in the list and is unread.
|
||||
//
|
||||
req = NewRequest(t, "GET", "/api/v1/notifications?all=true").
|
||||
AddTokenAuth(token)
|
||||
@@ -212,3 +212,23 @@ func TestAPINotificationPUT(t *testing.T) {
|
||||
assert.True(t, apiNL[0].Unread)
|
||||
assert.False(t, apiNL[0].Pinned)
|
||||
}
|
||||
|
||||
func TestAPINotificationPublicOnly(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
thread5 := unittest.AssertExistsAndLoadBean(t, &activities_model.Notification{ID: 5})
|
||||
|
||||
token := getUserToken(t, user2.Name, auth_model.AccessTokenScopeReadNotification, auth_model.AccessTokenScopePublicOnly)
|
||||
req := NewRequest(t, "GET", "/api/v1/notifications").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/notifications/new").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/notifications/threads/%d", thread5.ID)).
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
@@ -137,34 +137,45 @@ func TestAPIOrgGeneral(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("OrgEdit", func(t *testing.T) {
|
||||
org := api.EditOrgOption{
|
||||
FullName: "Org3 organization new full name",
|
||||
Description: "A new description",
|
||||
Website: "https://try.gitea.io/new",
|
||||
Location: "Beijing",
|
||||
Visibility: "private",
|
||||
}
|
||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
|
||||
assert.NotEqual(t, api.VisibleTypeLimited, org3.Visibility)
|
||||
|
||||
var apiOrg api.Organization
|
||||
DecodeJSON(t, resp, &apiOrg)
|
||||
org3Edit := api.EditOrgOption{
|
||||
FullName: new("new full name"),
|
||||
Description: new("new description"),
|
||||
Website: new("https://org3-new-website.example.com"),
|
||||
Location: new("new location"),
|
||||
Visibility: new("limited"),
|
||||
Email: new("org3-new-email@example.com"),
|
||||
}
|
||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org3Edit).AddTokenAuth(user1Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiOrg := DecodeJSON(t, resp, &api.Organization{})
|
||||
|
||||
assert.Equal(t, "org3", apiOrg.Name)
|
||||
assert.Equal(t, org.FullName, apiOrg.FullName)
|
||||
assert.Equal(t, org.Description, apiOrg.Description)
|
||||
assert.Equal(t, org.Website, apiOrg.Website)
|
||||
assert.Equal(t, org.Location, apiOrg.Location)
|
||||
assert.Equal(t, org.Visibility, apiOrg.Visibility)
|
||||
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
|
||||
assert.Equal(t, *org3Edit.Description, apiOrg.Description)
|
||||
assert.Equal(t, *org3Edit.Website, apiOrg.Website)
|
||||
assert.Equal(t, *org3Edit.Location, apiOrg.Location)
|
||||
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
|
||||
assert.Equal(t, *org3Edit.Email, apiOrg.Email)
|
||||
org3 = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
|
||||
assert.Equal(t, api.VisibleTypeLimited, org3.Visibility)
|
||||
|
||||
// empty email can clear the email, nil fields won't change the settings
|
||||
req = NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &api.EditOrgOption{
|
||||
Email: new(""),
|
||||
}).AddTokenAuth(user1Token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiOrg = DecodeJSON(t, resp, &api.Organization{})
|
||||
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
|
||||
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
|
||||
assert.Empty(t, apiOrg.Email)
|
||||
})
|
||||
|
||||
t.Run("OrgEditBadVisibility", func(t *testing.T) {
|
||||
t.Run("OrgEditInvalidVisibility", func(t *testing.T) {
|
||||
org := api.EditOrgOption{
|
||||
FullName: "Org3 organization new full name",
|
||||
Description: "A new description",
|
||||
Website: "https://try.gitea.io/new",
|
||||
Location: "Beijing",
|
||||
Visibility: "badvisibility",
|
||||
Visibility: new("invalid-visibility"),
|
||||
}
|
||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
arch_module "code.gitea.io/gitea/modules/packages/arch"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
arch_service "code.gitea.io/gitea/services/packages/arch"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
@@ -78,34 +79,6 @@ license = MIT`)
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
readIndexContent := func(r io.Reader) (map[string]string, error) {
|
||||
gzr, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := make(map[string]string)
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
for {
|
||||
hd, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf, err := io.ReadAll(tr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content[hd.Name] = string(buf)
|
||||
}
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
compressions := []string{"gz", "xz", "zst"}
|
||||
repositories := []string{"main", "testing", "with/slash", ""}
|
||||
@@ -204,7 +177,7 @@ license = MIT`)
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
content, err := readIndexContent(resp.Body)
|
||||
content, err := test.ReadAllTarGzContent(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
|
||||
desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)]
|
||||
@@ -256,7 +229,7 @@ license = MIT`)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/aarch64/%s", rootURL, repository, arch_service.IndexArchiveFilename))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
content, err := readIndexContent(resp.Body)
|
||||
content, err := test.ReadAllTarGzContent(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
|
||||
desc, has := content[fmt.Sprintf("%s-%s/desc", packageName, packageVersion)]
|
||||
@@ -311,7 +284,7 @@ license = MIT`)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/aarch64/%s", rootURL, arch_service.IndexArchiveFilename))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
content, err := readIndexContent(resp.Body)
|
||||
content, err := test.ReadAllTarGzContent(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, content, 2)
|
||||
|
||||
@@ -326,7 +299,7 @@ license = MIT`)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/aarch64/%s", rootURL, arch_service.IndexArchiveFilename))
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
content, err = readIndexContent(resp.Body)
|
||||
content, err = test.ReadAllTarGzContent(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, content, 2)
|
||||
_, has = content["gitea-test-1.0.0/desc"]
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
composer_module "code.gitea.io/gitea/modules/packages/composer"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/routers/api/packages/composer"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
@@ -27,6 +28,8 @@ func TestPackageComposer(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
otherUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 31})
|
||||
|
||||
vendorName := "gitea"
|
||||
projectName := "composer-package"
|
||||
@@ -251,5 +254,85 @@ func TestPackageComposer(t *testing.T) {
|
||||
assert.Equal(t, repo1.HTMLURL(), pkgs[0].Source.URL)
|
||||
assert.Equal(t, "git", pkgs[0].Source.Type)
|
||||
assert.Equal(t, packageVersion, pkgs[0].Source.Reference)
|
||||
|
||||
// Private repository links remain visible to callers who can access the repository.
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
err = packages.SetRepositoryLink(t.Context(), userPkgs[0].ID, repo2.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/p2/%s/%s.json", url, vendorName, projectName)).
|
||||
AddBasicAuth(user.Name)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
result = composer.PackageMetadataResponse{}
|
||||
DecodeJSON(t, resp, &result)
|
||||
pkgs = result.Packages[packageName]
|
||||
assert.Len(t, pkgs, 1)
|
||||
assert.Equal(t, repo2.HTMLURL(), pkgs[0].Source.URL)
|
||||
assert.Equal(t, "git", pkgs[0].Source.Type)
|
||||
assert.Equal(t, packageVersion, pkgs[0].Source.Reference)
|
||||
|
||||
// Callers without repository access still get the package metadata, but not the private source URL.
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/p2/%s/%s.json", url, vendorName, projectName)).
|
||||
AddBasicAuth(otherUser.Name)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
result = composer.PackageMetadataResponse{}
|
||||
DecodeJSON(t, resp, &result)
|
||||
pkgs = result.Packages[packageName]
|
||||
assert.Len(t, pkgs, 1)
|
||||
assert.Empty(t, pkgs[0].Source.URL)
|
||||
assert.Empty(t, pkgs[0].Source.Type)
|
||||
assert.Empty(t, pkgs[0].Source.Reference)
|
||||
})
|
||||
|
||||
t.Run("WebVisibilityBadge", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
listReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages", user.Name)).
|
||||
AddBasicAuth(user.Name)
|
||||
listResp := MakeRequest(t, listReq, http.StatusOK)
|
||||
listDoc := NewHTMLParser(t, listResp.Body)
|
||||
assert.Equal(t, 0, listDoc.Find(".flex-item-title .ui.basic.label").Length())
|
||||
|
||||
viewReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/composer/%s/%s", user.Name, neturl.PathEscape(packageName), neturl.PathEscape(packageVersion))).
|
||||
AddBasicAuth(user.Name)
|
||||
viewResp := MakeRequest(t, viewReq, http.StatusOK)
|
||||
viewDoc := NewHTMLParser(t, viewResp.Body)
|
||||
assert.Equal(t, 0, viewDoc.Find(".issue-title-header .ui.basic.label").Length())
|
||||
|
||||
privatePackageName := privateUser.Name + "/private-composer-package"
|
||||
privatePackageVersion := "1.0.0"
|
||||
privateContent := test.WriteZipArchive(map[string]string{
|
||||
"composer.json": `{
|
||||
"name": "` + privatePackageName + `",
|
||||
"description": "Private Package",
|
||||
"type": "` + packageType + `",
|
||||
"license": "` + packageLicense + `",
|
||||
"authors": [
|
||||
{
|
||||
"name": "` + packageAuthor + `"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
}).Bytes()
|
||||
privateUploadURL := fmt.Sprintf("%sapi/packages/%s/composer?version=%s", setting.AppURL, privateUser.Name, privatePackageVersion)
|
||||
|
||||
uploadReq := NewRequestWithBody(t, "PUT", privateUploadURL, bytes.NewReader(privateContent)).
|
||||
AddBasicAuth(privateUser.Name)
|
||||
MakeRequest(t, uploadReq, http.StatusCreated)
|
||||
privateSession := loginUser(t, privateUser.Name)
|
||||
|
||||
privateListReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages", privateUser.Name))
|
||||
privateListResp := privateSession.MakeRequest(t, privateListReq, http.StatusOK)
|
||||
privateListDoc := NewHTMLParser(t, privateListResp.Body)
|
||||
assert.Equal(t, 1, privateListDoc.Find(".flex-item-title .ui.basic.label").Length())
|
||||
assert.Equal(t, "Private", privateListDoc.Find(".flex-item-title .ui.basic.label").First().Text())
|
||||
|
||||
privateViewReq := NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/composer/%s/%s", privateUser.Name, neturl.PathEscape(privatePackageName), neturl.PathEscape(privatePackageVersion)))
|
||||
privateViewResp := privateSession.MakeRequest(t, privateViewReq, http.StatusOK)
|
||||
privateViewDoc := NewHTMLParser(t, privateViewResp.Body)
|
||||
assert.Equal(t, 1, privateViewDoc.Find(".issue-title-header .ui.basic.label").Length())
|
||||
assert.Equal(t, "Private", privateViewDoc.Find(".issue-title-header .ui.basic.label").First().Text())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -346,15 +346,16 @@ func TestPackageConan(t *testing.T) {
|
||||
pb, err := packages.GetBlobByID(t.Context(), pf.BlobID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
if pf.Name == conanfileName {
|
||||
switch pf.Name {
|
||||
case conanfileName:
|
||||
assert.True(t, pf.IsLead)
|
||||
|
||||
assert.Equal(t, int64(len(buildConanfileContent(name, version1))), pb.Size)
|
||||
} else if pf.Name == conaninfoName {
|
||||
case conaninfoName:
|
||||
assert.False(t, pf.IsLead)
|
||||
|
||||
assert.Equal(t, int64(len(contentConaninfo)), pb.Size)
|
||||
} else {
|
||||
default:
|
||||
assert.FailNow(t, "unknown file", "unknown file: %s", pf.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,34 +88,34 @@ func TestPackageContainer(t *testing.T) {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
defaultAuthenticateValues := []string{`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`}
|
||||
|
||||
wwwAuthenticateForPublic := []string{
|
||||
`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`,
|
||||
}
|
||||
wwwAuthenticateForRequiredSignIn := []string{
|
||||
`Bearer realm="` + setting.AppURL + `v2/token",service="container_registry",scope="*"`,
|
||||
`Basic realm="Gitea Container Registry"`,
|
||||
}
|
||||
t.Run("Anonymous", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", setting.AppURL+"v2")
|
||||
resp := MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
assert.ElementsMatch(t, defaultAuthenticateValues, resp.Header().Values("WWW-Authenticate"))
|
||||
assert.ElementsMatch(t, wwwAuthenticateForPublic, resp.Header().Values("WWW-Authenticate"))
|
||||
|
||||
req = NewRequest(t, "GET", setting.AppURL+"v2/token")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
tokenResponse := &TokenResponse{}
|
||||
DecodeJSON(t, resp, &tokenResponse)
|
||||
|
||||
assert.NotEmpty(t, tokenResponse.Token)
|
||||
|
||||
tokenResponse := DecodeJSON(t, resp, &TokenResponse{})
|
||||
require.NotEmpty(t, tokenResponse.Token)
|
||||
anonymousToken = "Bearer " + tokenResponse.Token
|
||||
|
||||
req = NewRequest(t, "GET", setting.AppURL+"v2").
|
||||
AddTokenAuth(anonymousToken)
|
||||
req = NewRequest(t, "GET", setting.AppURL+"v2").AddTokenAuth(anonymousToken)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
defer test.MockVariableValue(&setting.Service.RequireSignInViewStrict, true)()
|
||||
|
||||
req = NewRequest(t, "GET", setting.AppURL+"v2")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
resp = MakeRequest(t, req, http.StatusUnauthorized)
|
||||
assert.ElementsMatch(t, wwwAuthenticateForRequiredSignIn, resp.Header().Values("WWW-Authenticate"))
|
||||
|
||||
req = NewRequest(t, "GET", setting.AppURL+"v2/token")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
@@ -132,17 +132,13 @@ func TestPackageContainer(t *testing.T) {
|
||||
|
||||
req := NewRequest(t, "GET", setting.AppURL+"v2")
|
||||
resp := MakeRequest(t, req, http.StatusUnauthorized)
|
||||
assert.ElementsMatch(t, wwwAuthenticateForPublic, resp.Header().Values("WWW-Authenticate"))
|
||||
|
||||
assert.ElementsMatch(t, defaultAuthenticateValues, resp.Header().Values("WWW-Authenticate"))
|
||||
|
||||
req = NewRequest(t, "GET", setting.AppURL+"v2/token").
|
||||
AddBasicAuth(user.Name)
|
||||
req = NewRequest(t, "GET", setting.AppURL+"v2/token").AddBasicAuth(user.Name)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
tokenResponse := &TokenResponse{}
|
||||
DecodeJSON(t, resp, &tokenResponse)
|
||||
|
||||
tokenResponse := DecodeJSON(t, resp, &TokenResponse{})
|
||||
assert.NotEmpty(t, tokenResponse.Token)
|
||||
|
||||
pkgMeta, err := package_service.ParseAuthorizationToken(tokenResponse.Token)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, user.ID, pkgMeta.UserID)
|
||||
@@ -826,7 +822,6 @@ func TestPackageContainer(t *testing.T) {
|
||||
newOwnerName := "newUsername"
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
"name": newOwnerName,
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
@@ -836,7 +831,6 @@ func TestPackageContainer(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("Catalog[%s]", newOwnerName), checkCatalog(newOwnerName))
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"_csrf": GetUserCSRFToken(t, session),
|
||||
"name": user.Name,
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
neturl "net/url"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/packages"
|
||||
@@ -140,11 +142,12 @@ func TestPackageGeneric(t *testing.T) {
|
||||
t.Run("ServeDirect", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
if setting.Packages.Storage.Type == setting.MinioStorageType {
|
||||
switch setting.Packages.Storage.Type {
|
||||
case setting.MinioStorageType:
|
||||
defer test.MockVariableValue(&setting.Packages.Storage.MinioConfig.ServeDirect, true)()
|
||||
} else if setting.Packages.Storage.Type == setting.AzureBlobStorageType {
|
||||
case setting.AzureBlobStorageType:
|
||||
defer test.MockVariableValue(&setting.Packages.Storage.AzureBlobConfig.ServeDirect, true)()
|
||||
} else {
|
||||
default:
|
||||
t.Skip("Test skipped for non-Minio-storage and non-AzureBlob-storage.")
|
||||
}
|
||||
|
||||
@@ -163,6 +166,7 @@ func TestPackageGeneric(t *testing.T) {
|
||||
|
||||
resp2, err := (&http.Client{}).Get(location)
|
||||
assert.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
assert.Equal(t, http.StatusOK, resp2.StatusCode, location)
|
||||
|
||||
body, err := io.ReadAll(resp2.Body)
|
||||
@@ -171,6 +175,27 @@ func TestPackageGeneric(t *testing.T) {
|
||||
|
||||
checkDownloadCount(3)
|
||||
})
|
||||
|
||||
t.Run("WebAssetUsesFilename", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeGeneric)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pvs, 1)
|
||||
|
||||
pfs, err := packages.GetFilesByVersionID(t.Context(), pvs[0].ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, pfs)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/%s/-/packages/generic/%s/%s/files/%d", user.Name, neturl.PathEscape(packageName), neturl.PathEscape(packageVersion), pfs[0].ID))
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, content, resp.Body.Bytes())
|
||||
|
||||
disposition, params, err := mime.ParseMediaType(resp.Header().Get("Content-Disposition"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "attachment", disposition)
|
||||
assert.Equal(t, pfs[0].Name, params["filename"])
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
|
||||
@@ -160,7 +160,7 @@ func TestPackageMaven(t *testing.T) {
|
||||
t.Run("UploadVerifySHA1", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
t.Run("Missmatch", func(t *testing.T) {
|
||||
t.Run("Mismatch", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
putFile(t, fmt.Sprintf("/%s/%s.sha1", packageVersion, filename), "test", http.StatusBadRequest)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
neturl "net/url"
|
||||
@@ -930,4 +931,34 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("UploadMultipartForm", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
packageContent := createPackage(packageName, packageVersion).Bytes()
|
||||
|
||||
// Simulate dotnet nuget push which sends multipart/form-data with X-NuGet-ApiKey auth
|
||||
var body bytes.Buffer
|
||||
mpw := multipart.NewWriter(&body)
|
||||
part, err := mpw.CreateFormFile("package", "package.nupkg")
|
||||
assert.NoError(t, err)
|
||||
_, err = part.Write(packageContent)
|
||||
assert.NoError(t, err)
|
||||
err = mpw.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "PUT", url, &body).
|
||||
SetHeader("Content-Type", mpw.FormDataContentType())
|
||||
addNuGetAPIKeyHeader(req, writeToken)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeNuGet)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, pvs, 1)
|
||||
|
||||
// Clean up
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, packageVersion)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,15 +12,16 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
@@ -31,14 +32,24 @@ import (
|
||||
|
||||
func TestPackageRpm(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
packageName := "gitea-test"
|
||||
packageVersion := "1.0.2-1"
|
||||
packageArchitecture := "x86_64"
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
base64RpmPackageContent := `H4sICFayB2QCAGdpdGVhLXRlc3QtMS4wLjItMS14ODZfNjQucnBtAO2YV4gTQRjHJzl7wbNhhxVF
|
||||
// To build an RPM package, it needs tools lik "cpio", it's not easy to do in Golang.
|
||||
// So here we only use pre-built package contents. And to save space, the content is gzipped and base64 encoded.
|
||||
rpmContentFromGzipBase64 := func(t testing.TB, s string) []byte {
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
require.NoError(t, err)
|
||||
zr, err := gzip.NewReader(bytes.NewReader(b))
|
||||
require.NoError(t, err)
|
||||
content, err := io.ReadAll(zr)
|
||||
require.NoError(t, err)
|
||||
return content
|
||||
}
|
||||
packageRpmGzipBase64 := `H4sICFayB2QCAGdpdGVhLXRlc3QtMS4wLjItMS14ODZfNjQucnBtAO2YV4gTQRjHJzl7wbNhhxVF
|
||||
VNwk2zd2PdvZ9Sxnd3Z3NllNsmF3o6congVFsWFHRWwIImIXfRER0QcRfPBJEXvvBQvWSfZTT0VQ
|
||||
8TF/MuU33zcz3+zOJGEe73lyuQBRBWKWRzDrEddjuVAkxLMc+lsFUOWfm5bvvReAalWECg/TsivU
|
||||
dyKa0U61aVnl6wj0Uxe4nc8F92hZiaYE8CO/P0r7/Quegr0c7M/AvoCaGZEIWNGUqMHrhhGROIUT
|
||||
@@ -66,14 +77,17 @@ Mu0UFYgZ/bYnuvn/vz4wtCz8qMwsHUvP0PX3tbYFUctAPdrY6tiiDtcCddDECahx7SuVNP5dpmb5
|
||||
9tMDyaXb7OAlk5acuPn57ss9mw6Wym0m1Fq2cej7tUt2LL4/b8enXU2fndk+fvv57ndnt55/cQob
|
||||
7tpp/pEjDS7cGPZ6BY430+7danDq6f42Nw49b9F7zp6BiKpJb9s5P0AYN2+L159cnrur636rx+v1
|
||||
7ae1K28QbMMcqI8CqwIrgwg9nTOp8Oj9q81plUY7ZuwXN8Vvs8wbAAA=`
|
||||
rpmPackageContent, err := base64.StdEncoding.DecodeString(base64RpmPackageContent)
|
||||
assert.NoError(t, err)
|
||||
|
||||
zr, err := gzip.NewReader(bytes.NewReader(rpmPackageContent))
|
||||
assert.NoError(t, err)
|
||||
packageRpmContent := rpmContentFromGzipBase64(t, packageRpmGzipBase64)
|
||||
|
||||
content, err := io.ReadAll(zr)
|
||||
assert.NoError(t, err)
|
||||
decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v any) {
|
||||
t.Helper()
|
||||
|
||||
zr, err := gzip.NewReader(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, xml.NewDecoder(zr).Decode(v))
|
||||
}
|
||||
|
||||
rootURL := fmt.Sprintf("/api/packages/%s/rpm", user.Name)
|
||||
|
||||
@@ -99,7 +113,7 @@ gpgcheck=1
|
||||
gpgkey=%sapi/packages/%s/rpm/repository.key`,
|
||||
strings.Join(append([]string{user.LowerName}, groupParts...), "-"),
|
||||
strings.Join(append([]string{user.Name, setting.AppName}, groupParts...), " - "),
|
||||
util.URLJoin(setting.AppURL, groupURL),
|
||||
strings.TrimSuffix(setting.AppURL, "/")+groupURL,
|
||||
setting.AppURL,
|
||||
user.Name,
|
||||
)
|
||||
@@ -120,10 +134,10 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
|
||||
t.Run("Upload", func(t *testing.T) {
|
||||
url := groupURL + "/upload"
|
||||
|
||||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
|
||||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent))
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
|
||||
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
@@ -146,9 +160,9 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
|
||||
|
||||
pb, err := packages.GetBlobByID(t.Context(), pfs[0].BlobID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(len(content)), pb.Size)
|
||||
assert.Equal(t, int64(len(packageRpmContent)), pb.Size)
|
||||
|
||||
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
|
||||
req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusConflict)
|
||||
})
|
||||
@@ -159,12 +173,12 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
|
||||
// download the package without the file name
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s", groupURL, packageName, packageVersion, packageArchitecture))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, content, resp.Body.Bytes())
|
||||
assert.Equal(t, packageRpmContent, resp.Body.Bytes())
|
||||
|
||||
// download the package with a file name (it can be anything)
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s/any-file-name", groupURL, packageName, packageVersion, packageArchitecture))
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, content, resp.Body.Bytes())
|
||||
assert.Equal(t, packageRpmContent, resp.Body.Bytes())
|
||||
})
|
||||
|
||||
t.Run("Repository", func(t *testing.T) {
|
||||
@@ -248,15 +262,6 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
|
||||
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNATURE-----")
|
||||
})
|
||||
|
||||
decodeGzipXML := func(t testing.TB, resp *httptest.ResponseRecorder, v any) {
|
||||
t.Helper()
|
||||
|
||||
zr, err := gzip.NewReader(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, xml.NewDecoder(zr).Decode(v))
|
||||
}
|
||||
|
||||
t.Run("primary.xml.gz", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -331,7 +336,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
|
||||
assert.Equal(t, "sha256", p.Checksum.Type)
|
||||
assert.Equal(t, "f1d5d2ffcbe4a7568e98b864f40d923ecca084e9b9bcd5977ed6521c46d3fa4c", p.Checksum.Checksum)
|
||||
assert.Equal(t, "https://gitea.io", p.URL)
|
||||
assert.EqualValues(t, len(content), p.Size.Package)
|
||||
assert.EqualValues(t, len(packageRpmContent), p.Size.Package)
|
||||
assert.EqualValues(t, 13, p.Size.Installed)
|
||||
assert.EqualValues(t, 272, p.Size.Archive)
|
||||
assert.Equal(t, fmt.Sprintf("package/%s/%s/%s/%s", packageName, packageVersion, packageArchitecture, fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture)), p.Location.Href)
|
||||
@@ -421,6 +426,417 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Errata", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
type updateInfo struct {
|
||||
XMLName xml.Name `xml:"updates"`
|
||||
Xmlns string `xml:"xmlns,attr"`
|
||||
Updates []*rpm_module.Update `xml:"update"`
|
||||
}
|
||||
|
||||
errataURL := fmt.Sprintf("%s/package/%s/%s/errata", groupURL, packageName, packageVersion)
|
||||
|
||||
advisory := rpm_module.Update{
|
||||
From: "security@example.com",
|
||||
Status: "stable",
|
||||
Type: "security",
|
||||
Version: "1.0",
|
||||
ID: "CVE-2023-1234",
|
||||
Title: "Test Security Update",
|
||||
Severity: "Important",
|
||||
Description: "This is a test security update.",
|
||||
References: []*rpm_module.Reference{
|
||||
{
|
||||
Href: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-1234",
|
||||
ID: "CVE-2023-1234",
|
||||
Title: "CVE-2023-1234",
|
||||
Type: "cve",
|
||||
},
|
||||
},
|
||||
PkgList: []*rpm_module.Collection{
|
||||
{
|
||||
Short: "el9",
|
||||
Packages: []*rpm_module.UpdatePackage{
|
||||
{
|
||||
Arch: packageArchitecture,
|
||||
Name: packageName,
|
||||
Release: "1",
|
||||
Src: "gitea-test-1.0.2-1.src.rpm",
|
||||
Version: "1.0.2",
|
||||
Filename: fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
updates := []*rpm_module.Update{&advisory}
|
||||
body, err := json.Marshal(updates)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
url := groupURL + "/repodata"
|
||||
|
||||
// Check repomd.xml contains updateinfo
|
||||
req = NewRequest(t, "GET", url+"/repomd.xml")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
type testRepoData struct {
|
||||
Type string `xml:"type,attr"`
|
||||
}
|
||||
var repomd struct {
|
||||
Data []*testRepoData `xml:"data"`
|
||||
}
|
||||
err = xml.NewDecoder(resp.Body).Decode(&repomd)
|
||||
require.NoError(t, err)
|
||||
|
||||
found := slices.IndexFunc(repomd.Data, func(s *testRepoData) bool {
|
||||
return s.Type == "updateinfo"
|
||||
}) >= 0
|
||||
assert.True(t, found, "updateinfo not found in repomd.xml")
|
||||
|
||||
// Now check updateinfo.xml.gz
|
||||
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var result updateInfo
|
||||
decodeGzipXML(t, resp, &result)
|
||||
|
||||
assert.Equal(t, "http://linux.duke.edu/metadata/updateinfo", result.Xmlns)
|
||||
assert.Len(t, result.Updates, 1)
|
||||
assert.Equal(t, "CVE-2023-1234", result.Updates[0].ID)
|
||||
assert.NotEmpty(t, result.Updates[0].Issued.Date)
|
||||
assert.NotEmpty(t, result.Updates[0].Updated.Date)
|
||||
})
|
||||
|
||||
t.Run("InvalidJSON", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithBody(t, "POST", errataURL, strings.NewReader("invalid json")).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("NullElementsInJSON", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// Send a payload with null inside arrays
|
||||
payload := `[
|
||||
{
|
||||
"id": "CVE-2023-5678",
|
||||
"from": "security@example.com",
|
||||
"status": "stable",
|
||||
"type": "security",
|
||||
"version": "1.0",
|
||||
"title": "Test Null Elements",
|
||||
"severity": "Important",
|
||||
"description": "Test null elements",
|
||||
"pkg_list": [
|
||||
null,
|
||||
{
|
||||
"short": "el9",
|
||||
"packages": [
|
||||
null,
|
||||
{
|
||||
"arch": "x86_64",
|
||||
"name": "gitea",
|
||||
"release": "1",
|
||||
"src": "gitea-1.0.0-1.src.rpm",
|
||||
"version": "1.0.0",
|
||||
"filename": "gitea-1.0.0-1.x86_64.rpm"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]`
|
||||
|
||||
req := NewRequestWithBody(t, "POST", errataURL, strings.NewReader(payload)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Verify it was stored correctly (skipping nulls)
|
||||
url := groupURL + "/repodata"
|
||||
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var result updateInfo
|
||||
decodeGzipXML(t, resp, &result)
|
||||
|
||||
// We need to find the new advisory CVE-2023-5678
|
||||
var newAdvisory *rpm_module.Update
|
||||
for _, u := range result.Updates {
|
||||
if u.ID == "CVE-2023-5678" {
|
||||
newAdvisory = u
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NotNil(t, newAdvisory)
|
||||
assert.Len(t, newAdvisory.PkgList, 1)
|
||||
assert.Equal(t, "el9", newAdvisory.PkgList[0].Short)
|
||||
assert.Len(t, newAdvisory.PkgList[0].Packages, 1)
|
||||
assert.Equal(t, "gitea", newAdvisory.PkgList[0].Packages[0].Name)
|
||||
})
|
||||
|
||||
t.Run("PackageNotFound", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
badURL := fmt.Sprintf("%s/package/%s/non-existent-version/errata", groupURL, packageName)
|
||||
updates := []*rpm_module.Update{&advisory}
|
||||
body, err := json.Marshal(updates)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", badURL, bytes.NewReader(body)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("MergeAdvisories", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// Upload a second advisory with the same ID but a different package
|
||||
advisory2 := advisory
|
||||
advisory2.PkgList = []*rpm_module.Collection{
|
||||
{
|
||||
Short: "el9",
|
||||
Packages: []*rpm_module.UpdatePackage{
|
||||
{
|
||||
Arch: packageArchitecture,
|
||||
Name: "another-package",
|
||||
Release: "1",
|
||||
Src: "another-package-1.0.0-1.src.rpm",
|
||||
Version: "1.0.0",
|
||||
Filename: "another-package-1.0.0-1.x86_64.rpm",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updates := []*rpm_module.Update{&advisory2}
|
||||
body, err := json.Marshal(updates)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Check updateinfo.xml.gz again
|
||||
url := groupURL + "/repodata"
|
||||
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var result updateInfo
|
||||
decodeGzipXML(t, resp, &result)
|
||||
|
||||
var targetUpdate *rpm_module.Update
|
||||
for _, u := range result.Updates {
|
||||
if u.ID == "CVE-2023-1234" {
|
||||
targetUpdate = u
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NotNil(t, targetUpdate)
|
||||
// Verify that package lists are merged into the same collection
|
||||
assert.Len(t, targetUpdate.PkgList, 1)
|
||||
assert.Len(t, targetUpdate.PkgList[0].Packages, 2)
|
||||
assert.Equal(t, "another-package", result.Updates[0].PkgList[0].Packages[1].Name)
|
||||
})
|
||||
|
||||
t.Run("NewCollection", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// Upload a third advisory with the same ID but a different collection
|
||||
advisory3 := advisory
|
||||
advisory3.PkgList = []*rpm_module.Collection{
|
||||
{
|
||||
Short: "el8",
|
||||
Packages: []*rpm_module.UpdatePackage{
|
||||
{
|
||||
Arch: packageArchitecture,
|
||||
Name: packageName,
|
||||
Release: "1",
|
||||
Src: "gitea-test-1.0.2-1.src.rpm",
|
||||
Version: "1.0.2",
|
||||
Filename: fmt.Sprintf("%s-%s.%s.rpm", packageName, packageVersion, packageArchitecture),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
updates := []*rpm_module.Update{&advisory3}
|
||||
body, err := json.Marshal(updates)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Check updateinfo.xml.gz again
|
||||
url := groupURL + "/repodata"
|
||||
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var result updateInfo
|
||||
decodeGzipXML(t, resp, &result)
|
||||
|
||||
var targetUpdate *rpm_module.Update
|
||||
for _, u := range result.Updates {
|
||||
if u.ID == "CVE-2023-1234" {
|
||||
targetUpdate = u
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NotNil(t, targetUpdate)
|
||||
// Verify that we now have 2 collections
|
||||
assert.Len(t, targetUpdate.PkgList, 2)
|
||||
// We need to be careful with order, map iteration is random
|
||||
// Let's check both exist
|
||||
shorts := []string{targetUpdate.PkgList[0].Short, targetUpdate.PkgList[1].Short}
|
||||
assert.Contains(t, shorts, "el9")
|
||||
assert.Contains(t, shorts, "el8")
|
||||
})
|
||||
|
||||
t.Run("Idempotency", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
updates := []*rpm_module.Update{&advisory}
|
||||
body, err := json.Marshal(updates)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Post twice
|
||||
req := NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequestWithBody(t, "POST", errataURL, bytes.NewReader(body)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Check updateinfo.xml.gz
|
||||
url := groupURL + "/repodata"
|
||||
req = NewRequest(t, "GET", url+"/updateinfo.xml.gz")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var result updateInfo
|
||||
decodeGzipXML(t, resp, &result)
|
||||
|
||||
var targetUpdate *rpm_module.Update
|
||||
for _, u := range result.Updates {
|
||||
if u.ID == "CVE-2023-1234" {
|
||||
targetUpdate = u
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NotNil(t, targetUpdate)
|
||||
assert.Len(t, targetUpdate.PkgList, 2)
|
||||
|
||||
var el9Coll *rpm_module.Collection
|
||||
for _, coll := range targetUpdate.PkgList {
|
||||
if coll.Short == "el9" {
|
||||
el9Coll = coll
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NotNil(t, el9Coll)
|
||||
assert.Len(t, el9Coll.Packages, 2)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("NoArch", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
noarchPackageName := "gitea-noarch-test"
|
||||
noarchPackageVersion := "1.0.0-1"
|
||||
noarchPackageGzipBase64 := `H4sICKC05mkAA2dpdGVhLW5vYXJjaC10ZXN0LTEuMC4wLTEubm9hcmNoLnJwbQDtmLtrFEEcx+dy` +
|
||||
`p0ZROYliFMUTLJJiz53HPgbBBwZR0Jgiglo5sztzLt6Lu0uIYmEhFhFUsBUJahd8gHZWKvgnpLES` +
|
||||
`VIwYo43a6Dm7+zvfhbXsF+ZmP/N7zM5y03wXZt89yyOjbiXqKGHVG6IVnLQ6qt2xcNku2xZG/6wc` +
|
||||
`WvL70qXbr3PwuAyh4gMz74TnW2YumqJVZl76vQPKrQEeTjn/2swFM6rAb9N61Ezr84sQPwfx9xA/` +
|
||||
`b8Il6nEpWSgwVz7lrqOZ9oWjqKZSShIQT/k4cHWIAtcJHNsR1OGKSp8w5QcMM+4SiSV1OSY293wi` +
|
||||
`TAMmmBauL1XoeJ7mXAqhafL6BXzzSd+N2eFP9x8/nHt25u7CBbN49t8/YKZMmTJlypQpU6ZMmTJl` +
|
||||
`ypTpP1biiXS73Sso8TR+8U1KCOU+mHkXSnyN3HPICc3oh5yeTxL7Jn3A88Brgd8Ab0Q/fJTlZmwC` +
|
||||
`XgC2gd+h1FfZDbwI9SPAHyB+EPgjxMeAP0O/ceAvED8B/BVYp1xYC1wDXg/nuww8CPvFvlHePG6A` +
|
||||
`+D3gjcBd4KG0X24d1B9N63Nw3sKxND9XApaQPwQcAm8HVsAMWANz4CrwjpQHrsJ+8D0GnkIcvsfA` +
|
||||
`C9j/OPBLyL8GPA/xmZj3oj/8OZT4cwij0WStFK+VmiI4JSoKjf8EZdMgevVgRjbqqg1/mEMHxtGR` +
|
||||
`erupgkhHKkTVqD4xhdLuf27VswLL7VZQbjVrf3mZ9KVX9IZJqkZyaG+j1mypdluF+6KqGhU11R5G` +
|
||||
`EItXRqKKKf6xNiZOVxsiSW7vF5NqrKV0NDWMqNmfWRixsptYkgx+iV1O/Mn+nldpHSYlq4KCZtRA` +
|
||||
`lTNRE3E4lDWp6mGjZaUHTWomOtrykdCUKxxgLjTzfKG0J2wqFfeVFjyMzT1CfC255lRRn5HQDT2J` +
|
||||
`mcDMdQRzeNqL0NBmhEimlTDpnoeV7xGPYSmpZ3vcljQIbYf6SmjJXEwoVT6xpc+k4wkS/7fSC97t` +
|
||||
`xvcCFbdcTO92X57apoHNSquLs6s3b3s0uHXPpes77+yZnp5eaa7m3PxbJ3YYvwFme3wbyRUAAA==`
|
||||
noarchRpmPackageContent := rpmContentFromGzipBase64(t, noarchPackageGzipBase64)
|
||||
// Upload the noarch RPM
|
||||
req := NewRequestWithBody(t, "PUT", groupURL+"/upload", bytes.NewReader(noarchRpmPackageContent)).AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// The noarch package should appear in primary.xml when any arch is requested.
|
||||
// At this point the group contains the existing x86_64 package + this noarch one.
|
||||
req = NewRequest(t, "GET", groupURL+"/repodata/primary.xml.gz")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
type PackageSummary struct {
|
||||
Name string `xml:"name"`
|
||||
Architecture string `xml:"arch"`
|
||||
}
|
||||
type PrimaryMetadata struct {
|
||||
XMLName xml.Name `xml:"metadata"`
|
||||
PackageCount int `xml:"packages,attr"`
|
||||
Packages []PackageSummary `xml:"package"`
|
||||
}
|
||||
|
||||
var primary PrimaryMetadata
|
||||
decodeGzipXML(t, resp, &primary)
|
||||
|
||||
// Both the arch-specific package uploaded earlier and the noarch one must be present.
|
||||
assert.Equal(t, 2, primary.PackageCount)
|
||||
assert.Len(t, primary.Packages, 2)
|
||||
|
||||
archNames := make([]string, 0, len(primary.Packages))
|
||||
for _, p := range primary.Packages {
|
||||
archNames = append(archNames, p.Architecture)
|
||||
}
|
||||
assert.Contains(t, archNames, packageArchitecture) // x86_64 from the Upload subtest
|
||||
assert.Contains(t, archNames, "noarch")
|
||||
|
||||
// noarch package must be downloadable regardless of the arch path used.
|
||||
for _, arch := range []string{"noarch", "x86_64", "aarch64", "my_arch"} {
|
||||
req = NewRequest(t, "GET", fmt.Sprintf(
|
||||
"%s/package/%s/%s/%s",
|
||||
groupURL, noarchPackageName, noarchPackageVersion, arch,
|
||||
))
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
// Clean up: delete via the canonical noarch path.
|
||||
req = NewRequest(t, "DELETE", fmt.Sprintf(
|
||||
"%s/package/%s/%s/noarch",
|
||||
groupURL, noarchPackageName, noarchPackageVersion,
|
||||
)).AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// After deletion, the noarch package must no longer be reachable via any arch.
|
||||
for _, arch := range []string{"noarch", "x86_64", "aarch64"} {
|
||||
req = NewRequest(t, "GET", fmt.Sprintf(
|
||||
"%s/package/%s/%s/%s",
|
||||
groupURL, noarchPackageName, noarchPackageVersion, arch,
|
||||
))
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
// The x86_64 package from the Upload subtest must still be present.
|
||||
req = NewRequest(t, "GET", groupURL+"/repodata/primary.xml.gz")
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var primaryAfter PrimaryMetadata
|
||||
decodeGzipXML(t, resp, &primaryAfter)
|
||||
assert.Equal(t, 1, primaryAfter.PackageCount)
|
||||
assert.Equal(t, packageArchitecture, primaryAfter.Packages[0].Architecture)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -442,7 +858,7 @@ gpgkey=%sapi/packages/%s/rpm/repository.key`,
|
||||
t.Run("UploadSign", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
url := groupURL + "/upload?sign=true"
|
||||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
|
||||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(packageRpmContent)).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
swift_module "code.gitea.io/gitea/modules/packages/swift"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
swift_router "code.gitea.io/gitea/routers/api/packages/swift"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
@@ -35,9 +36,26 @@ func TestPackageSwift(t *testing.T) {
|
||||
packageID := packageScope + "." + packageName
|
||||
packageVersion := "1.0.3"
|
||||
packageVersion2 := "1.0.4"
|
||||
packageVersion3 := "1.0.5"
|
||||
packageAuthor := "KN4CK3R"
|
||||
packageDescription := "Gitea Test Package"
|
||||
packageRepositoryURL := "https://gitea.io/gitea/gitea"
|
||||
packageCodeRepositoryURL := "https://gitea.io/gitea/gitea" // this one is not used as a property, it is meta
|
||||
packageLicenseURL := "https://opensource.org/license/mit"
|
||||
packageRepositoryURL1 := "https://gitea.io/gitea/repo"
|
||||
packageRepositoryURLs := []string{packageRepositoryURL1, "https://gitea.io/gitea/repo.git", "ssh://git@gitea.io/gitea/repo.git"}
|
||||
makePackageMetadataJSON := func(ver string) string {
|
||||
tmpl := `{
|
||||
"name":"` + packageName + `",
|
||||
"version":"%s",
|
||||
"description":"` + packageDescription + `",
|
||||
"codeRepository":"` + packageCodeRepositoryURL + `",
|
||||
"licenseURL":"` + packageLicenseURL + `",
|
||||
"author":{"givenName":"` + packageAuthor + `"},
|
||||
"repositoryURLs":["` + strings.Join(packageRepositoryURLs, `","`) + `"]
|
||||
}`
|
||||
return fmt.Sprintf(tmpl, ver)
|
||||
}
|
||||
|
||||
contentManifest1 := "// swift-tools-version:5.7\n//\n// Package.swift"
|
||||
contentManifest2 := "// swift-tools-version:5.6\n//\n// Package@swift-5.6.swift"
|
||||
|
||||
@@ -146,7 +164,7 @@ func TestPackageSwift(t *testing.T) {
|
||||
"Package.swift": contentManifest1,
|
||||
"Package@swift-5.6.swift": contentManifest2,
|
||||
}),
|
||||
`{"name":"`+packageName+`","version":"`+packageVersion+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
|
||||
makePackageMetadataJSON(packageVersion),
|
||||
)
|
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeSwift)
|
||||
@@ -164,8 +182,8 @@ func TestPackageSwift(t *testing.T) {
|
||||
assert.Len(t, metadata.Manifests, 2)
|
||||
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
|
||||
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
|
||||
assert.Len(t, pd.VersionProperties, 1)
|
||||
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
||||
assert.Len(t, pd.VersionProperties, 3)
|
||||
assert.Equal(t, packageRepositoryURL1, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
||||
|
||||
pfs, err := packages.GetFilesByVersionID(t.Context(), pvs[0].ID)
|
||||
assert.NoError(t, err)
|
||||
@@ -234,7 +252,7 @@ func TestPackageSwift(t *testing.T) {
|
||||
"Package.swift": contentManifest1,
|
||||
"Package@swift-5.6.swift": contentManifest2,
|
||||
}),
|
||||
`{"name":"`+packageName+`","version":"`+packageVersion2+`","description":"`+packageDescription+`","codeRepository":"`+packageRepositoryURL+`","author":{"givenName":"`+packageAuthor+`"},"repositoryURLs":["`+packageRepositoryURL+`"]}`,
|
||||
makePackageMetadataJSON(packageVersion2),
|
||||
)
|
||||
|
||||
pvs, err := packages.GetVersionsByPackageType(t.Context(), user.ID, packages.TypeSwift)
|
||||
@@ -252,8 +270,8 @@ func TestPackageSwift(t *testing.T) {
|
||||
assert.Len(t, metadata.Manifests, 2)
|
||||
assert.Equal(t, contentManifest1, metadata.Manifests[""].Content)
|
||||
assert.Equal(t, contentManifest2, metadata.Manifests["5.6"].Content)
|
||||
assert.Len(t, pd.VersionProperties, 1)
|
||||
assert.Equal(t, packageRepositoryURL, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
||||
assert.Len(t, pd.VersionProperties, 3)
|
||||
assert.Equal(t, packageRepositoryURL1, pd.VersionProperties.GetByName(swift_module.PropertyRepositoryURL))
|
||||
|
||||
pfs, err := packages.GetFilesByVersionID(t.Context(), thisPackageVersion.ID)
|
||||
assert.NoError(t, err)
|
||||
@@ -318,7 +336,7 @@ func TestPackageSwift(t *testing.T) {
|
||||
AddBasicAuth(user.Name)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
assert.Equal(t, body, resp.Body.String())
|
||||
assert.JSONEq(t, body, resp.Body.String())
|
||||
})
|
||||
|
||||
t.Run("PackageVersionMetadata", func(t *testing.T) {
|
||||
@@ -354,8 +372,11 @@ func TestPackageSwift(t *testing.T) {
|
||||
assert.Equal(t, packageVersion, result.Metadata.Version)
|
||||
assert.Equal(t, packageDescription, result.Metadata.Description)
|
||||
assert.Equal(t, "Swift", result.Metadata.ProgrammingLanguage.Name)
|
||||
assert.Equal(t, packageLicenseURL, result.Metadata.LicenseURL)
|
||||
require.NotNil(t, result.Metadata.Author)
|
||||
assert.Equal(t, packageAuthor, result.Metadata.Author.Name)
|
||||
assert.Equal(t, packageAuthor, result.Metadata.Author.GivenName)
|
||||
assert.ElementsMatch(t, packageRepositoryURLs, result.Metadata.RepositoryURLs)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s.json", url, packageScope, packageName, packageVersion)).
|
||||
AddBasicAuth(user.Name)
|
||||
@@ -364,6 +385,41 @@ func TestPackageSwift(t *testing.T) {
|
||||
assert.Equal(t, body, resp.Body.String())
|
||||
})
|
||||
|
||||
t.Run("UploadEmptyJSONMetadata", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
uploadURL := fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion3)
|
||||
var body bytes.Buffer
|
||||
mpw := multipart.NewWriter(&body)
|
||||
|
||||
part, err := mpw.CreateFormFile("source-archive", "source-archive.zip")
|
||||
require.NoError(t, err)
|
||||
_, err = io.Copy(part, test.WriteZipArchive(map[string]string{
|
||||
"Package.swift": contentManifest1,
|
||||
"Package@swift-5.6.swift": contentManifest2,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mpw.WriteField("metadata", "{}"))
|
||||
require.NoError(t, mpw.Close())
|
||||
|
||||
req := NewRequestWithBody(t, "PUT", uploadURL, &body).
|
||||
SetHeader("Content-Type", mpw.FormDataContentType()).
|
||||
SetHeader("Accept", swift_router.AcceptJSON).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s/%s", url, packageScope, packageName, packageVersion3)).
|
||||
AddBasicAuth(user.Name).
|
||||
SetHeader("Accept", swift_router.AcceptJSON)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
result := DecodeJSON(t, resp, &swift_router.PackageVersionMetadataResponse{})
|
||||
|
||||
assert.Nil(t, result.Metadata.Author)
|
||||
assert.Empty(t, result.Metadata.RepositoryURLs)
|
||||
assert.Empty(t, result.Metadata.CodeRepository)
|
||||
assert.Empty(t, result.Metadata.LicenseURL)
|
||||
})
|
||||
|
||||
t.Run("DownloadManifest", func(t *testing.T) {
|
||||
manifestURL := fmt.Sprintf("%s/%s/%s/%s/Package.swift", url, packageScope, packageName, packageVersion)
|
||||
|
||||
@@ -421,7 +477,7 @@ func TestPackageSwift(t *testing.T) {
|
||||
req = NewRequest(t, "GET", url+"/identifiers?url=https://unknown.host/")
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequest(t, "GET", url+"/identifiers?url="+packageRepositoryURL).
|
||||
req = NewRequest(t, "GET", url+"/identifiers?url="+packageRepositoryURL1).
|
||||
SetHeader("Accept", swift_router.AcceptJSON)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -85,6 +85,50 @@ func TestPackageAPI(t *testing.T) {
|
||||
assert.Equal(t, user.Name, p.Creator.UserName)
|
||||
})
|
||||
|
||||
t.Run("DeleteEntirePackage", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
packageName := "test-package-entire-delete"
|
||||
for _, version := range []string{"1.0.1", "1.0.2"} {
|
||||
url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/file.bin", user.Name, packageName, version)
|
||||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{1})).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/generic/%s", user.Name, packageName)).
|
||||
AddTokenAuth(tokenWritePackage)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s", user.Name, packageName)).
|
||||
AddTokenAuth(tokenReadPackage)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
|
||||
t.Run("DeletePackageVersion", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
packageName := "test-package-version-delete"
|
||||
for _, version := range []string{"1.0.1", "1.0.2"} {
|
||||
url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/file.bin", user.Name, packageName, version)
|
||||
req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{1})).
|
||||
AddBasicAuth(user.Name)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
|
||||
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.1", user.Name, packageName)).
|
||||
AddTokenAuth(tokenWritePackage)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.1", user.Name, packageName)).
|
||||
AddTokenAuth(tokenReadPackage)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/1.0.2", user.Name, packageName)).
|
||||
AddTokenAuth(tokenReadPackage)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("ListPackageVersions", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestAPIPrivateServ(t *testing.T) {
|
||||
assert.Empty(t, results)
|
||||
|
||||
// Add reading deploy key
|
||||
deployKey, err := asymkey_model.AddDeployKey(ctx, 19, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", true)
|
||||
deployKey, err := asymkey_model.AddDeployKey(ctx, 19 /* repo id */, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Can pull from repo we're a deploy key for
|
||||
@@ -106,17 +106,17 @@ func TestAPIPrivateServ(t *testing.T) {
|
||||
assert.Empty(t, results)
|
||||
|
||||
// Cannot pull from a private repo we're not associated with
|
||||
results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "")
|
||||
results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "")
|
||||
assert.Error(t, extra.Error)
|
||||
assert.Empty(t, results)
|
||||
|
||||
// Cannot pull from a public repo we're not associated with
|
||||
results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "")
|
||||
results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "")
|
||||
assert.Error(t, extra.Error)
|
||||
assert.Empty(t, results)
|
||||
|
||||
// Add writing deploy key
|
||||
deployKey, err = asymkey_model.AddDeployKey(ctx, 20, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
|
||||
deployKey, err = asymkey_model.AddDeployKey(ctx, 20 /* repo id */, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Cannot push to a private repo with reading key
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIUserReposPublicOnly(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
|
||||
req := NewRequest(t, "GET", "/api/v1/user/repos").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var repos []api.Repository
|
||||
DecodeJSON(t, resp, &repos)
|
||||
assert.NotEmpty(t, repos)
|
||||
for _, repo := range repos {
|
||||
assert.False(t, repo.Private)
|
||||
}
|
||||
assert.NotContains(t, repoNames(repos), "user2/repo2")
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/users/user2/repos").
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &repos)
|
||||
assert.NotEmpty(t, repos)
|
||||
for _, repo := range repos {
|
||||
assert.False(t, repo.Private)
|
||||
}
|
||||
assert.NotContains(t, repoNames(repos), "user2/repo2")
|
||||
}
|
||||
|
||||
func repoNames(repos []api.Repository) []string {
|
||||
names := make([]string, 0, len(repos))
|
||||
for _, repo := range repos {
|
||||
names = append(names, repo.FullName)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func TestAPIRepoByIDPublicOnly(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
|
||||
req := NewRequest(t, "GET", "/api/v1/repositories/1").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/repositories/2").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIActivityFeedsPublicOnly(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser)
|
||||
req := NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var activities []api.Activity
|
||||
DecodeJSON(t, resp, &activities)
|
||||
assert.NotEmpty(t, activities)
|
||||
|
||||
publicToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
|
||||
req = NewRequest(t, "GET", "/api/v1/users/user2/activities/feeds").
|
||||
AddTokenAuth(publicToken)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &activities)
|
||||
assertPublicActivitiesOnly(t, activities)
|
||||
|
||||
orgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization)
|
||||
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
|
||||
AddTokenAuth(orgToken)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &activities)
|
||||
assert.NotEmpty(t, activities)
|
||||
|
||||
publicOrgToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopePublicOnly)
|
||||
req = NewRequest(t, "GET", "/api/v1/orgs/org3/activities/feeds").
|
||||
AddTokenAuth(publicOrgToken)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &activities)
|
||||
assertPublicActivitiesOnly(t, activities)
|
||||
}
|
||||
|
||||
func assertPublicActivitiesOnly(t *testing.T, activities []api.Activity) {
|
||||
t.Helper()
|
||||
|
||||
for _, activity := range activities {
|
||||
assert.False(t, activity.IsPrivate)
|
||||
if activity.Repo != nil {
|
||||
assert.False(t, activity.Repo.Private)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,11 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -362,6 +364,79 @@ func TestAPIPullReviewRequest(t *testing.T) {
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func TestAPIPullReviewCommentResolveEndpoints(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
ctx := t.Context()
|
||||
pullIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3})
|
||||
require.NoError(t, pullIssue.LoadAttributes(ctx))
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pullIssue.RepoID})
|
||||
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: pullIssue.PosterID})
|
||||
require.NoError(t, pullIssue.LoadPullRequest(ctx))
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
latestCommitID, err := gitRepo.GetRefCommitID(pullIssue.PullRequest.GetGitHeadRefName())
|
||||
require.NoError(t, err)
|
||||
|
||||
codeComment, err := pull_service.CreateCodeComment(ctx, doer, gitRepo, pullIssue, 1, "resolve comment", "README.md", false, 0, latestCommitID, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, codeComment)
|
||||
|
||||
session := loginUser(t, doer.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
resolveURL := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/resolve", repo.OwnerName, repo.Name, codeComment.ID)
|
||||
unresolveURL := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/unresolve", repo.OwnerName, repo.Name, codeComment.ID)
|
||||
|
||||
req := NewRequest(t, http.MethodPost, resolveURL).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Verify comment is resolved
|
||||
updatedComment, err := issues_model.GetCommentByID(ctx, codeComment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotZero(t, updatedComment.ResolveDoerID)
|
||||
assert.Equal(t, doer.ID, updatedComment.ResolveDoerID)
|
||||
|
||||
// Resolving again should be idempotent
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
req = NewRequest(t, http.MethodPost, unresolveURL).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Verify comment is unresolved
|
||||
updatedComment, err = issues_model.GetCommentByID(ctx, codeComment.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, updatedComment.ResolveDoerID)
|
||||
|
||||
// Unresolving again should be idempotent
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// Non-existing comment ID
|
||||
req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/999999/resolve", repo.OwnerName, repo.Name)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// Non-code-comment
|
||||
plainComment, err := issue_service.CreateIssueComment(ctx, doer, repo, pullIssue, "not a review comment", nil)
|
||||
require.NoError(t, err)
|
||||
req = NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls/comments/%d/resolve", repo.OwnerName, repo.Name, plainComment.ID)).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
// Test permission check: use a user without write access for target repo to test 403 response
|
||||
unauthorizedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
|
||||
require.NotEqual(t, pullIssue.PosterID, unauthorizedUser.ID)
|
||||
|
||||
unauthorizedSession := loginUser(t, unauthorizedUser.Name)
|
||||
unauthorizedToken := getTokenForLoggedInUser(t, unauthorizedSession, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req = NewRequest(t, http.MethodGet, fmt.Sprintf("/api/v1/repos/%s/%s/issues/comments/%d", repo.OwnerName, repo.Name, plainComment.ID)).AddTokenAuth(unauthorizedToken)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
req = NewRequest(t, http.MethodPost, resolveURL).AddTokenAuth(unauthorizedToken)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func TestAPIPullReviewStayDismissed(t *testing.T) {
|
||||
// This test against issue https://github.com/go-gitea/gitea/issues/28542
|
||||
// where old reviews surface after a review request got dismissed.
|
||||
@@ -423,7 +498,7 @@ func TestAPIPullReviewStayDismissed(t *testing.T) {
|
||||
pullIssue.ID, user8.ID, 1, 1, 2, false)
|
||||
|
||||
// user8 dismiss review
|
||||
permUser8, err := access_model.GetUserRepoPermission(t.Context(), pullIssue.Repo, user8)
|
||||
permUser8, err := access_model.GetIndividualUserRepoPermission(t.Context(), pullIssue.Repo, user8)
|
||||
assert.NoError(t, err)
|
||||
_, err = issue_service.ReviewRequest(t.Context(), pullIssue, user8, &permUser8, user8, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -270,13 +270,20 @@ func TestAPICreatePullSuccess(t *testing.T) {
|
||||
owner11 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo11.OwnerID})
|
||||
|
||||
session := loginUser(t, owner11.Name)
|
||||
prTitle := "test pull request title " + time.Now().String()
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &api.CreatePullRequestOption{
|
||||
Head: owner11.Name + ":master",
|
||||
Base: "master",
|
||||
Title: "create a failure pr",
|
||||
Title: prTitle,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// Also test that AllowMaintainerEdit is true by default, the "false" case is covered by TestAPICreatePullBasePermission
|
||||
prIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: prTitle})
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: prIssue.ID})
|
||||
assert.True(t, pr.AllowMaintainerEdit)
|
||||
|
||||
MakeRequest(t, req, http.StatusUnprocessableEntity) // second request should fail
|
||||
}
|
||||
|
||||
@@ -290,11 +297,14 @@ func TestAPICreatePullBasePermission(t *testing.T) {
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||
|
||||
session := loginUser(t, user4.Name)
|
||||
prTitle := "test pull request title " + time.Now().String()
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
opts := &api.CreatePullRequestOption{
|
||||
Head: repo11.OwnerName + ":master",
|
||||
Base: "master",
|
||||
Title: "create a failure pr",
|
||||
Title: prTitle,
|
||||
|
||||
AllowMaintainerEdit: new(false),
|
||||
}
|
||||
req := NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
@@ -306,6 +316,11 @@ func TestAPICreatePullBasePermission(t *testing.T) {
|
||||
// create again
|
||||
req = NewRequestWithJSON(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/pulls", owner10.Name, repo10.Name), &opts).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
// Also test that AllowMaintainerEdit is set to false, the default "true" case is covered by TestAPICreatePullSuccess
|
||||
prIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: prTitle})
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: prIssue.ID})
|
||||
assert.False(t, pr.AllowMaintainerEdit)
|
||||
}
|
||||
|
||||
func TestAPICreatePullHeadPermission(t *testing.T) {
|
||||
@@ -442,9 +457,8 @@ func TestAPIEditPull(t *testing.T) {
|
||||
Base: "master",
|
||||
Title: title,
|
||||
}).AddTokenAuth(token)
|
||||
apiPull := new(api.PullRequest)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
DecodeJSON(t, resp, apiPull)
|
||||
apiPull := DecodeJSON(t, resp, &api.PullRequest{})
|
||||
assert.Equal(t, "master", apiPull.Base.Name)
|
||||
|
||||
newTitle := "edit a this pr"
|
||||
@@ -455,8 +469,9 @@ func TestAPIEditPull(t *testing.T) {
|
||||
Body: &newBody,
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
DecodeJSON(t, resp, apiPull)
|
||||
apiPull = DecodeJSON(t, resp, &api.PullRequest{})
|
||||
assert.Equal(t, "feature/1", apiPull.Base.Name)
|
||||
|
||||
// check comment history
|
||||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: apiPull.ID})
|
||||
err := pull.LoadIssue(t.Context())
|
||||
@@ -468,6 +483,62 @@ func TestAPIEditPull(t *testing.T) {
|
||||
Base: "not-exist",
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
t.Run("PullContentVersion", func(t *testing.T) {
|
||||
testAPIPullContentVersion(t, pull.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIPullContentVersion(t *testing.T, pullID int64) {
|
||||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pullID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: pull.BaseRepoID})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, owner.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner.Name, repo.Name, pull.Index)
|
||||
|
||||
t.Run("ResponseIncludesContentVersion", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiPull := DecodeJSON(t, resp, &api.PullRequest{})
|
||||
assert.GreaterOrEqual(t, apiPull.ContentVersion, 0)
|
||||
})
|
||||
|
||||
t.Run("EditWithCorrectVersion", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", urlStr).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
before := DecodeJSON(t, resp, &api.PullRequest{})
|
||||
req = NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
|
||||
Body: new("updated body with correct version"),
|
||||
ContentVersion: new(before.ContentVersion),
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
after := DecodeJSON(t, resp, &api.PullRequest{})
|
||||
assert.Equal(t, "updated body with correct version", after.Body)
|
||||
assert.Greater(t, after.ContentVersion, before.ContentVersion)
|
||||
})
|
||||
|
||||
t.Run("EditWithWrongVersion", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
|
||||
Body: new("should fail"),
|
||||
ContentVersion: new(99999),
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusConflict)
|
||||
})
|
||||
|
||||
t.Run("EditWithoutVersion", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequestWithJSON(t, "PATCH", urlStr, api.EditPullRequestOption{
|
||||
Body: new("edit without version succeeds"),
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusCreated)
|
||||
})
|
||||
}
|
||||
|
||||
func doAPIGetPullFiles(ctx APITestContext, pr *api.PullRequest, callback func(*testing.T, []*api.ChangedFile)) func(*testing.T) {
|
||||
|
||||
@@ -13,15 +13,15 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) {
|
||||
func testAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) {
|
||||
// Limit the allowed release types (since by default there is no restriction)
|
||||
defer test.MockVariableValue(&setting.Repository.Release.AllowedTypes, ".exe")()
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 9})
|
||||
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
|
||||
@@ -38,3 +38,34 @@ func TestAPIEditReleaseAttachmentWithUnallowedFile(t *testing.T) {
|
||||
|
||||
session.MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
func testAPIDraftReleaseAttachmentAccess(t *testing.T) {
|
||||
attachment := unittest.AssertExistsAndLoadBean(t, &repo_model.Attachment{ID: 13})
|
||||
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: attachment.ReleaseID})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: attachment.RepoID})
|
||||
repoOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
reader := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
listURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets", repoOwner.Name, repo.Name, release.ID)
|
||||
getURL := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets/%d", repoOwner.Name, repo.Name, release.ID, attachment.ID)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", listURL), http.StatusNotFound)
|
||||
MakeRequest(t, NewRequest(t, "GET", getURL), http.StatusNotFound)
|
||||
|
||||
readerToken := getUserToken(t, reader.LowerName, auth_model.AccessTokenScopeReadRepository)
|
||||
MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(readerToken), http.StatusNotFound)
|
||||
MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(readerToken), http.StatusNotFound)
|
||||
|
||||
ownerReadToken := getUserToken(t, repoOwner.LowerName, auth_model.AccessTokenScopeReadRepository)
|
||||
MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(ownerReadToken), http.StatusNotFound)
|
||||
MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(ownerReadToken), http.StatusNotFound)
|
||||
ownerToken := getUserToken(t, repoOwner.LowerName, auth_model.AccessTokenScopeWriteRepository)
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(ownerToken), http.StatusOK)
|
||||
var attachments []*api.Attachment
|
||||
DecodeJSON(t, resp, &attachments)
|
||||
if assert.Len(t, attachments, 1) {
|
||||
assert.Equal(t, attachment.ID, attachments[0].ID)
|
||||
}
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(ownerToken), http.StatusOK)
|
||||
}
|
||||
|
||||
@@ -29,12 +29,22 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIListReleases(t *testing.T) {
|
||||
func TestAPIReleaseRead(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("DraftReleaseAttachmentAccess", testAPIDraftReleaseAttachmentAccess)
|
||||
t.Run("ListReleasesWithWriteToken", testAPIListReleasesWithWriteToken)
|
||||
t.Run("ListReleasesWithReadToken", testAPIListReleasesWithReadToken)
|
||||
t.Run("GetDraftRelease", testAPIGetDraftRelease)
|
||||
t.Run("GetLatestRelease", testAPIGetLatestRelease)
|
||||
t.Run("GetReleaseByTag", testAPIGetReleaseByTag)
|
||||
t.Run("GetDraftReleaseByTag", testAPIGetDraftReleaseByTag)
|
||||
t.Run("EditReleaseAttachmentWithUnallowedFile", testAPIEditReleaseAttachmentWithUnallowedFile) // failed attempt, so it is also a read test
|
||||
}
|
||||
|
||||
func testAPIListReleasesWithWriteToken(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeReadRepository)
|
||||
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name))
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
|
||||
@@ -81,6 +91,72 @@ func TestAPIListReleases(t *testing.T) {
|
||||
testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft")
|
||||
}
|
||||
|
||||
func testAPIListReleasesWithReadToken(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name))
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
|
||||
var apiReleases []*api.Release
|
||||
DecodeJSON(t, resp, &apiReleases)
|
||||
if assert.Len(t, apiReleases, 2) {
|
||||
for _, release := range apiReleases {
|
||||
switch release.ID {
|
||||
case 1:
|
||||
assert.False(t, release.IsDraft)
|
||||
assert.False(t, release.IsPrerelease)
|
||||
assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/1/assets"), release.UploadURL)
|
||||
case 5:
|
||||
assert.False(t, release.IsDraft)
|
||||
assert.True(t, release.IsPrerelease)
|
||||
assert.True(t, strings.HasSuffix(release.UploadURL, "/api/v1/repos/user2/repo1/releases/5/assets"), release.UploadURL)
|
||||
default:
|
||||
assert.NoError(t, fmt.Errorf("unexpected release: %v", release))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test filter
|
||||
testFilterByLen := func(auth bool, query url.Values, expectedLength int, msgAndArgs ...string) {
|
||||
link.RawQuery = query.Encode()
|
||||
req := NewRequest(t, "GET", link.String())
|
||||
if auth {
|
||||
req.AddTokenAuth(token)
|
||||
}
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &apiReleases)
|
||||
assert.Len(t, apiReleases, expectedLength, msgAndArgs)
|
||||
}
|
||||
|
||||
testFilterByLen(false, url.Values{"draft": {"true"}}, 0, "anon should not see drafts")
|
||||
testFilterByLen(true, url.Values{"draft": {"true"}}, 0, "repo owner with read token should not see drafts")
|
||||
testFilterByLen(true, url.Values{"draft": {"false"}}, 2, "exclude drafts")
|
||||
testFilterByLen(true, url.Values{"draft": {"false"}, "pre-release": {"false"}}, 1, "exclude drafts and pre-releases")
|
||||
testFilterByLen(true, url.Values{"pre-release": {"true"}}, 1, "only get pre-release")
|
||||
testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft")
|
||||
}
|
||||
|
||||
func testAPIGetDraftRelease(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
release := unittest.AssertExistsAndLoadBean(t, &repo_model.Release{ID: 4})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
reader := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d", owner.Name, repo.Name, release.ID)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", urlStr), http.StatusNotFound)
|
||||
|
||||
readerToken := getUserToken(t, reader.LowerName, auth_model.AccessTokenScopeReadRepository)
|
||||
MakeRequest(t, NewRequest(t, "GET", urlStr).AddTokenAuth(readerToken), http.StatusNotFound)
|
||||
|
||||
ownerToken := getUserToken(t, owner.LowerName, auth_model.AccessTokenScopeWriteRepository)
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", urlStr).AddTokenAuth(ownerToken), http.StatusOK)
|
||||
var apiRelease api.Release
|
||||
DecodeJSON(t, resp, &apiRelease)
|
||||
assert.Equal(t, release.Title, apiRelease.Title)
|
||||
}
|
||||
|
||||
func createNewReleaseUsingAPI(t *testing.T, token string, owner *user_model.User, repo *repo_model.Repository, name, target, title, desc string) *api.Release {
|
||||
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases", owner.Name, repo.Name)
|
||||
req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateReleaseOption{
|
||||
@@ -230,9 +306,7 @@ func TestAPICreateReleaseGivenInvalidTarget(t *testing.T) {
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func TestAPIGetLatestRelease(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testAPIGetLatestRelease(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
@@ -245,9 +319,7 @@ func TestAPIGetLatestRelease(t *testing.T) {
|
||||
assert.Equal(t, "testing-release", release.Title)
|
||||
}
|
||||
|
||||
func TestAPIGetReleaseByTag(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testAPIGetReleaseByTag(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
@@ -271,9 +343,7 @@ func TestAPIGetReleaseByTag(t *testing.T) {
|
||||
assert.NotEmpty(t, err.Message)
|
||||
}
|
||||
|
||||
func TestAPIGetDraftReleaseByTag(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testAPIGetDraftReleaseByTag(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
@@ -335,7 +405,7 @@ func TestAPIDeleteReleaseByTagName(t *testing.T) {
|
||||
|
||||
func TestAPIUploadAssetRelease(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Attachment.MaxSize, 1)()
|
||||
defer test.MockVariableValue(&setting.Repository.Release.FileMaxSize, 1)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
@@ -352,7 +422,7 @@ func TestAPIUploadAssetRelease(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
const filename = "image.png"
|
||||
|
||||
performUpload := func(t *testing.T, uploadURL string, buf []byte, expectedStatus int) *httptest.ResponseRecorder {
|
||||
performUpload := func(t *testing.T, uploadURL string, _ []byte, _ int) *httptest.ResponseRecorder {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
part, err := writer.CreateFormFile("attachment", filename)
|
||||
|
||||
@@ -29,10 +29,10 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
session := loginUser(t, user1.LowerName)
|
||||
|
||||
// public only token should be forbidden
|
||||
// public-only token cannot see a private repo
|
||||
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeWriteRepository)
|
||||
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches", repo3.Name)) // a plain repo
|
||||
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
|
||||
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", link.String()).AddTokenAuth(token), http.StatusOK)
|
||||
@@ -46,7 +46,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
|
||||
assert.Equal(t, "master", branches[1].Name)
|
||||
|
||||
link2, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch", repo3.Name))
|
||||
MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
|
||||
|
||||
resp = MakeRequest(t, NewRequest(t, "GET", link2.String()).AddTokenAuth(token), http.StatusOK)
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
@@ -55,7 +55,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
|
||||
assert.NoError(t, json.Unmarshal(bs, &branch))
|
||||
assert.Equal(t, "test_branch", branch.Name)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "POST", link.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
|
||||
|
||||
req := NewRequest(t, "POST", link.String()).AddTokenAuth(token)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
@@ -81,7 +81,7 @@ func TestAPIRepoBranchesPlain(t *testing.T) {
|
||||
|
||||
link3, _ := url.Parse(fmt.Sprintf("/api/v1/repos/org3/%s/branches/test_branch2", repo3.Name))
|
||||
MakeRequest(t, NewRequest(t, "DELETE", link3.String()), http.StatusNotFound)
|
||||
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(publicOnlyToken), http.StatusNotFound)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "DELETE", link3.String()).AddTokenAuth(token), http.StatusNoContent)
|
||||
assert.NoError(t, err)
|
||||
@@ -121,10 +121,10 @@ func TestAPIRepoBranchesMirror(t *testing.T) {
|
||||
resp = MakeRequest(t, req, http.StatusForbidden)
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
|
||||
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
|
||||
|
||||
resp = MakeRequest(t, NewRequest(t, "DELETE", link2.String()).AddTokenAuth(token), http.StatusForbidden)
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}\n", string(bs))
|
||||
assert.JSONEq(t, "{\"message\":\"Git Repository is a mirror.\",\"url\":\""+setting.AppURL+"api/swagger\"}", string(bs))
|
||||
}
|
||||
|
||||
@@ -5,46 +5,60 @@ package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPICompareBranches(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
session2 := loginUser(t, "user2")
|
||||
token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
// Login as User2.
|
||||
session := loginUser(t, user.Name)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
t.Run("CompareBranches", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
t.Run("CompareBranches", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token2)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiResp := DecodeJSON(t, resp, &api.Compare{})
|
||||
assert.Equal(t, 2, apiResp.TotalCommits)
|
||||
assert.Len(t, apiResp.Commits, 2)
|
||||
})
|
||||
|
||||
var apiResp *api.Compare
|
||||
DecodeJSON(t, resp, &apiResp)
|
||||
t.Run("CompareCommits", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
assert.Equal(t, 2, apiResp.TotalCommits)
|
||||
assert.Len(t, apiResp.Commits, 2)
|
||||
})
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token2)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiResp := DecodeJSON(t, resp, &api.Compare{})
|
||||
assert.Equal(t, 1, apiResp.TotalCommits)
|
||||
assert.Len(t, apiResp.Commits, 1)
|
||||
})
|
||||
|
||||
t.Run("CompareCommits", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
t.Run("CompareForkOnlyCommit", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
var apiResp *api.Compare
|
||||
DecodeJSON(t, resp, &apiResp)
|
||||
user13 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13})
|
||||
repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
|
||||
user13Sess := loginUser(t, "user13")
|
||||
user13Token := getTokenForLoggedInUser(t, user13Sess, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
assert.Equal(t, 1, apiResp.TotalCommits)
|
||||
assert.Len(t, apiResp.Commits, 1)
|
||||
_, err := createFileInBranch(user13, repo11, createFileInBranchOptions{OldBranch: "master", NewBranch: "new-branch"}, map[string]string{"file.txt": "content"})
|
||||
require.NoError(t, err)
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/user12/repo10/compare/master...user13:new-branch").AddTokenAuth(user13Token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiResp := DecodeJSON(t, resp, &api.Compare{})
|
||||
assert.Equal(t, 1, apiResp.TotalCommits)
|
||||
assert.Len(t, apiResp.Commits, 1)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -418,5 +418,20 @@ func TestAPIRepoEdit(t *testing.T) {
|
||||
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo1.Name), &repoEditOption).
|
||||
AddTokenAuth(token4)
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
|
||||
// Test updating pull request settings without setting has_pull_requests
|
||||
repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
url = fmt.Sprintf("/api/v1/repos/%s/%s", user2.Name, repo1.Name)
|
||||
req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{
|
||||
DefaultDeleteBranchAfterMerge: &bTrue,
|
||||
}).AddTokenAuth(token2)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &repo)
|
||||
assert.True(t, repo.DefaultDeleteBranchAfterMerge)
|
||||
// reset
|
||||
req = NewRequestWithJSON(t, "PATCH", url, &api.EditRepoOption{
|
||||
DefaultDeleteBranchAfterMerge: &bFalse,
|
||||
}).AddTokenAuth(token2)
|
||||
_ = MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -53,8 +52,8 @@ func getCreateFileOptions() api.CreateFileOptions {
|
||||
func normalizeFileContentResponseCommitTime(c *api.ContentsResponse) {
|
||||
// decoded JSON response may contain different timezone from the one parsed by git commit
|
||||
// so we need to normalize the time to UTC to make "assert.Equal" pass
|
||||
c.LastCommitterDate = util.ToPointer(c.LastCommitterDate.UTC())
|
||||
c.LastAuthorDate = util.ToPointer(c.LastAuthorDate.UTC())
|
||||
c.LastCommitterDate = new(c.LastCommitterDate.UTC())
|
||||
c.LastAuthorDate = new(c.LastAuthorDate.UTC())
|
||||
}
|
||||
|
||||
type apiFileResponseInfo struct {
|
||||
@@ -75,9 +74,9 @@ func getExpectedFileResponseForCreate(info apiFileResponseInfo) *api.FileRespons
|
||||
Name: path.Base(info.treePath),
|
||||
Path: info.treePath,
|
||||
SHA: sha,
|
||||
LastCommitSHA: util.ToPointer(info.lastCommitSHA),
|
||||
LastCommitterDate: util.ToPointer(info.lastCommitterWhen),
|
||||
LastAuthorDate: util.ToPointer(info.lastAuthorWhen),
|
||||
LastCommitSHA: new(info.lastCommitSHA),
|
||||
LastCommitterDate: new(info.lastCommitterWhen),
|
||||
LastAuthorDate: new(info.lastAuthorWhen),
|
||||
Size: 16,
|
||||
Type: "file",
|
||||
Encoding: &encoding,
|
||||
|
||||
@@ -20,21 +20,19 @@ import (
|
||||
|
||||
func getDeleteFileOptions() *api.DeleteFileOptions {
|
||||
return &api.DeleteFileOptions{
|
||||
FileOptionsWithSHA: api.FileOptionsWithSHA{
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: "master",
|
||||
NewBranchName: "master",
|
||||
Message: "Removing the file new/file.txt",
|
||||
Author: api.Identity{
|
||||
Name: "John Doe",
|
||||
Email: "johndoe@example.com",
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: "Jane Doe",
|
||||
Email: "janedoe@example.com",
|
||||
},
|
||||
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: "master",
|
||||
NewBranchName: "master",
|
||||
Message: "Removing the file new/file.txt",
|
||||
Author: api.Identity{
|
||||
Name: "John Doe",
|
||||
Email: "johndoe@example.com",
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: "Jane Doe",
|
||||
Email: "janedoe@example.com",
|
||||
},
|
||||
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -25,8 +24,9 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
|
||||
|
||||
// Test with LFS
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
createLFSTestRepository(t, "repo-lfs-test")
|
||||
httpContext := NewAPITestContext(t, "user2", "repo-lfs-test", auth_model.AccessTokenScopeWriteRepository)
|
||||
doAPICreateRepository(httpContext, false, func(t *testing.T, repository api.Repository) {
|
||||
t.Run("repo-lfs-test", func(t *testing.T) {
|
||||
u.Path = httpContext.GitPath()
|
||||
dstPath := t.TempDir()
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestAPIGetRawFileOrLFS(t *testing.T) {
|
||||
|
||||
lfs := lfsCommitAndPushTest(t, dstPath, testFileSizeSmall)[0]
|
||||
|
||||
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/media/"+lfs)
|
||||
reqLFS := NewRequest(t, "GET", "/api/v1/repos/user2/repo-lfs-test/media/"+lfs).AddTokenAuth(httpContext.Token)
|
||||
respLFS := MakeRequestNilResponseRecorder(t, reqLFS, http.StatusOK)
|
||||
assert.Equal(t, testFileSizeSmall, respLFS.Length)
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -19,6 +20,9 @@ import (
|
||||
|
||||
type createFileInBranchOptions struct {
|
||||
OldBranch, NewBranch string
|
||||
CommitMessage string
|
||||
CommitterName string
|
||||
CommitterEmail string
|
||||
}
|
||||
|
||||
func testCreateFileInBranch(t *testing.T, user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) *api.FilesResponse {
|
||||
@@ -29,7 +33,17 @@ func testCreateFileInBranch(t *testing.T, user *user_model.User, repo *repo_mode
|
||||
|
||||
func createFileInBranch(user *user_model.User, repo *repo_model.Repository, createOpts createFileInBranchOptions, files map[string]string) (*api.FilesResponse, error) {
|
||||
ctx := context.TODO()
|
||||
opts := &files_service.ChangeRepoFilesOptions{OldBranch: createOpts.OldBranch, NewBranch: createOpts.NewBranch}
|
||||
opts := &files_service.ChangeRepoFilesOptions{
|
||||
OldBranch: createOpts.OldBranch,
|
||||
NewBranch: createOpts.NewBranch,
|
||||
Message: createOpts.CommitMessage,
|
||||
}
|
||||
if createOpts.CommitterName != "" || createOpts.CommitterEmail != "" {
|
||||
opts.Committer = &files_service.IdentityOptions{
|
||||
GitUserName: createOpts.CommitterName,
|
||||
GitUserEmail: createOpts.CommitterEmail,
|
||||
}
|
||||
}
|
||||
for path, content := range files {
|
||||
opts.Files = append(opts.Files, &files_service.ChangeRepoFile{
|
||||
Operation: "create",
|
||||
@@ -59,7 +73,7 @@ func deleteFileInBranch(user *user_model.User, repo *repo_model.Repository, tree
|
||||
func createOrReplaceFileInBranch(user *user_model.User, repo *repo_model.Repository, treePath, branchName, content string) error {
|
||||
_, err := deleteFileInBranch(user, repo, treePath, branchName)
|
||||
|
||||
if err != nil && !files_service.IsErrRepoFileDoesNotExist(err) {
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -28,21 +27,19 @@ func getUpdateFileOptions() *api.UpdateFileOptions {
|
||||
content := "This is updated text"
|
||||
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
|
||||
return &api.UpdateFileOptions{
|
||||
FileOptionsWithSHA: api.FileOptionsWithSHA{
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: "master",
|
||||
NewBranchName: "master",
|
||||
Message: "My update of new/file.txt",
|
||||
Author: api.Identity{
|
||||
Name: "John Doe",
|
||||
Email: "johndoe@example.com",
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: "Anne Doe",
|
||||
Email: "annedoe@example.com",
|
||||
},
|
||||
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
||||
FileOptions: api.FileOptions{
|
||||
BranchName: "master",
|
||||
NewBranchName: "master",
|
||||
Message: "My update of new/file.txt",
|
||||
Author: api.Identity{
|
||||
Name: "John Doe",
|
||||
Email: "johndoe@example.com",
|
||||
},
|
||||
Committer: api.Identity{
|
||||
Name: "Anne Doe",
|
||||
Email: "annedoe@example.com",
|
||||
},
|
||||
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
|
||||
},
|
||||
ContentBase64: contentEncoded,
|
||||
}
|
||||
@@ -61,9 +58,9 @@ func getExpectedFileResponseForUpdate(info apiFileResponseInfo) *api.FileRespons
|
||||
Name: path.Base(info.treePath),
|
||||
Path: info.treePath,
|
||||
SHA: sha,
|
||||
LastCommitSHA: util.ToPointer(info.lastCommitSHA),
|
||||
LastCommitterDate: util.ToPointer(info.lastCommitterWhen),
|
||||
LastAuthorDate: util.ToPointer(info.lastAuthorWhen),
|
||||
LastCommitSHA: new(info.lastCommitSHA),
|
||||
LastCommitterDate: new(info.lastCommitterWhen),
|
||||
LastAuthorDate: new(info.lastAuthorWhen),
|
||||
Type: "file",
|
||||
Size: 20,
|
||||
Encoding: &encoding,
|
||||
@@ -180,6 +177,15 @@ func TestAPIUpdateFile(t *testing.T) {
|
||||
assert.Equal(t, expectedDownloadURL, *fileResponse.Content.DownloadURL)
|
||||
assert.Equal(t, updateFileOptions.Message+"\n", fileResponse.Commit.Message)
|
||||
|
||||
// Test updating a file without SHA (should create the file)
|
||||
updateFileOptions = getUpdateFileOptions()
|
||||
updateFileOptions.SHA = ""
|
||||
req = NewRequestWithJSON(t, "PUT", "/api/v1/repos/user2/repo1/contents/update-create.txt", &updateFileOptions).AddTokenAuth(token2)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
DecodeJSON(t, resp, &fileResponse)
|
||||
assert.Equal(t, "08bd14b2e2852529157324de9c226b3364e76136", fileResponse.Content.SHA)
|
||||
assert.Equal(t, setting.AppURL+"user2/repo1/raw/branch/master/update-create.txt", *fileResponse.Content.DownloadURL)
|
||||
|
||||
// Test updating a file and renaming it
|
||||
updateFileOptions = getUpdateFileOptions()
|
||||
updateFileOptions.BranchName = repo1.DefaultBranch
|
||||
|
||||
@@ -161,9 +161,88 @@ func TestAPIChangeFiles(t *testing.T) {
|
||||
assert.Equal(t, expectedUpdateHTMLURL, *filesResponse.Files[1].HTMLURL)
|
||||
assert.Equal(t, expectedUpdateDownloadURL, *filesResponse.Files[1].DownloadURL)
|
||||
assert.Nil(t, filesResponse.Files[2])
|
||||
|
||||
assert.Equal(t, changeFilesOptions.Message+"\n", filesResponse.Commit.Message)
|
||||
|
||||
// Test fails creating a file in a branch that already exists without force
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.BranchName = repo1.DefaultBranch
|
||||
changeFilesOptions.NewBranchName = "develop"
|
||||
changeFilesOptions.ForcePush = false
|
||||
fileID++
|
||||
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
createFile(user2, repo1, updateTreePath)
|
||||
createFile(user2, repo1, deleteTreePath)
|
||||
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
|
||||
AddTokenAuth(token2)
|
||||
resp = MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||
assert.Contains(t, resp.Body.String(), `"message":"branch already exists [name: develop]"`)
|
||||
|
||||
// Test succeeds creating a file in a branch that already exists with force
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.BranchName = repo1.DefaultBranch
|
||||
changeFilesOptions.NewBranchName = "develop"
|
||||
changeFilesOptions.ForcePush = true
|
||||
fileID++
|
||||
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
createFile(user2, repo1, updateTreePath)
|
||||
createFile(user2, repo1, deleteTreePath)
|
||||
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
|
||||
AddTokenAuth(token2)
|
||||
resp = MakeRequest(t, req, http.StatusCreated)
|
||||
DecodeJSON(t, resp, &filesResponse)
|
||||
expectedCreateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/develop/new/file%d.txt", fileID)
|
||||
expectedCreateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/develop/new/file%d.txt", fileID)
|
||||
expectedUpdateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/develop/update/file%d.txt", fileID)
|
||||
expectedUpdateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/develop/update/file%d.txt", fileID)
|
||||
assert.Equal(t, expectedCreateSHA, filesResponse.Files[0].SHA)
|
||||
assert.Equal(t, expectedCreateHTMLURL, *filesResponse.Files[0].HTMLURL)
|
||||
assert.Equal(t, expectedCreateDownloadURL, *filesResponse.Files[0].DownloadURL)
|
||||
assert.Equal(t, expectedUpdateSHA, filesResponse.Files[1].SHA)
|
||||
assert.Equal(t, expectedUpdateHTMLURL, *filesResponse.Files[1].HTMLURL)
|
||||
assert.Equal(t, expectedUpdateDownloadURL, *filesResponse.Files[1].DownloadURL)
|
||||
assert.Nil(t, filesResponse.Files[2])
|
||||
assert.Equal(t, changeFilesOptions.Message+"\n", filesResponse.Commit.Message)
|
||||
|
||||
// Test fails creating a file in a branch that already exists with force and branch protection enabled
|
||||
protectionReq := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections", &api.BranchProtection{
|
||||
RuleName: "develop",
|
||||
BranchName: "develop",
|
||||
Priority: 1,
|
||||
EnablePush: true,
|
||||
EnableForcePush: false,
|
||||
}).AddTokenAuth(token2)
|
||||
MakeRequest(t, protectionReq, http.StatusCreated)
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.BranchName = repo1.DefaultBranch
|
||||
changeFilesOptions.NewBranchName = "develop"
|
||||
changeFilesOptions.ForcePush = true
|
||||
fileID++
|
||||
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
|
||||
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
|
||||
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
|
||||
changeFilesOptions.Files[0].Path = createTreePath
|
||||
changeFilesOptions.Files[1].Path = updateTreePath
|
||||
changeFilesOptions.Files[2].Path = deleteTreePath
|
||||
createFile(user2, repo1, updateTreePath)
|
||||
createFile(user2, repo1, deleteTreePath)
|
||||
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
|
||||
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
|
||||
AddTokenAuth(token2)
|
||||
resp = MakeRequest(t, req, http.StatusForbidden)
|
||||
assert.Contains(t, resp.Body.String(), `"message":"branch develop is protected from force push"`)
|
||||
|
||||
// Test updating a file and renaming it
|
||||
changeFilesOptions = getChangeFilesOptions()
|
||||
changeFilesOptions.BranchName = repo1.DefaultBranch
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -35,9 +34,9 @@ func getExpectedContentsListResponseForContents(ref, refType, lastCommitSHA stri
|
||||
Name: path.Base(treePath),
|
||||
Path: treePath,
|
||||
SHA: sha,
|
||||
LastCommitSHA: util.ToPointer(lastCommitSHA),
|
||||
LastCommitterDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||
LastAuthorDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||
LastCommitSHA: new(lastCommitSHA),
|
||||
LastCommitterDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||
LastAuthorDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||
Type: "file",
|
||||
Size: 30,
|
||||
URL: &selfURL,
|
||||
@@ -80,7 +79,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) {
|
||||
|
||||
// Make a new branch in repo1
|
||||
newBranch := "test_branch"
|
||||
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, gitRepo, repo1.DefaultBranch, newBranch)
|
||||
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, repo1.DefaultBranch, newBranch)
|
||||
assert.NoError(t, err)
|
||||
|
||||
commitID, _ := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -34,17 +33,17 @@ func getExpectedContentsResponseForContents(ref, refType, lastCommitSHA string)
|
||||
Name: treePath,
|
||||
Path: treePath,
|
||||
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
|
||||
LastCommitSHA: util.ToPointer(lastCommitSHA),
|
||||
LastCommitterDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||
LastAuthorDate: util.ToPointer(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||
LastCommitSHA: new(lastCommitSHA),
|
||||
LastCommitterDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||
LastAuthorDate: new(time.Date(2017, time.March, 19, 16, 47, 59, 0, time.FixedZone("", -14400))),
|
||||
Type: "file",
|
||||
Size: 30,
|
||||
Encoding: util.ToPointer("base64"),
|
||||
Content: util.ToPointer("IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"),
|
||||
Encoding: new("base64"),
|
||||
Content: new("IyByZXBvMQoKRGVzY3JpcHRpb24gZm9yIHJlcG8x"),
|
||||
URL: &selfURL,
|
||||
HTMLURL: &htmlURL,
|
||||
GitURL: &gitURL,
|
||||
DownloadURL: util.ToPointer(setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath),
|
||||
DownloadURL: new(setting.AppURL + "user2/repo1/raw/" + refType + "/" + ref + "/" + treePath),
|
||||
Links: &api.FileLinksResponse{
|
||||
Self: &selfURL,
|
||||
GitURL: &gitURL,
|
||||
@@ -61,7 +60,7 @@ func TestAPIGetContents(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func testAPIGetContents(t *testing.T, u *url.URL) {
|
||||
func testAPIGetContents(t *testing.T, _ *url.URL) {
|
||||
/*** SETUP ***/
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
|
||||
@@ -85,7 +84,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) {
|
||||
|
||||
// Make a new branch in repo1
|
||||
newBranch := "test_branch"
|
||||
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, gitRepo, repo1.DefaultBranch, newBranch)
|
||||
err = repo_service.CreateNewBranch(t.Context(), user2, repo1, repo1.DefaultBranch, newBranch)
|
||||
require.NoError(t, err)
|
||||
|
||||
commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch)
|
||||
@@ -256,8 +255,8 @@ func testAPIGetContentsExt(t *testing.T) {
|
||||
assert.Equal(t, "jpeg.jpg", respFile.Name)
|
||||
assert.Nil(t, respFile.Encoding)
|
||||
assert.Nil(t, respFile.Content)
|
||||
assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize)
|
||||
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
|
||||
assert.Equal(t, new(int64(107)), respFile.LfsSize)
|
||||
assert.Equal(t, new("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
|
||||
})
|
||||
t.Run("FileContents", func(t *testing.T) {
|
||||
// by default, no file content or commit info is returned
|
||||
@@ -296,7 +295,7 @@ func testAPIGetContentsExt(t *testing.T) {
|
||||
assert.NotNil(t, respFile.Content)
|
||||
assert.Nil(t, contentsResponse.FileContents.LastCommitSHA)
|
||||
assert.Nil(t, contentsResponse.FileContents.LastCommitMessage)
|
||||
assert.Equal(t, util.ToPointer(int64(107)), respFile.LfsSize)
|
||||
assert.Equal(t, util.ToPointer("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
|
||||
assert.Equal(t, new(int64(107)), respFile.LfsSize)
|
||||
assert.Equal(t, new("0b8d8b5f15046343fd32f451df93acc2bdd9e6373be478b968e4cad6b6647351"), respFile.LfsOid)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,10 +12,8 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -30,8 +28,8 @@ func TestAPIGitTags(t *testing.T) {
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
// Set up git config for the tagger
|
||||
_ = gitcmd.NewCommand("config", "user.name").AddDynamicArguments(user.Name).Run(t.Context(), &gitcmd.RunOpts{Dir: repo.RepoPath()})
|
||||
_ = gitcmd.NewCommand("config", "user.email").AddDynamicArguments(user.Email).Run(t.Context(), &gitcmd.RunOpts{Dir: repo.RepoPath()})
|
||||
_ = gitrepo.GitConfigSet(t.Context(), repo, "user.name", user.Name)
|
||||
_ = gitrepo.GitConfigSet(t.Context(), repo, "user.email", user.Email)
|
||||
|
||||
gitRepo, _ := gitrepo.OpenRepository(t.Context(), repo)
|
||||
defer gitRepo.Close()
|
||||
@@ -59,7 +57,7 @@ func TestAPIGitTags(t *testing.T) {
|
||||
assert.Equal(t, aTagMessage+"\n", tag.Message)
|
||||
assert.Equal(t, user.Name, tag.Tagger.Name)
|
||||
assert.Equal(t, user.Email, tag.Tagger.Email)
|
||||
assert.Equal(t, util.URLJoin(repo.APIURL(), "git/tags", aTag.ID.String()), tag.URL)
|
||||
assert.Equal(t, repo.APIURL()+"/git/tags/"+aTag.ID.String(), tag.URL)
|
||||
|
||||
// Should NOT work for lightweight tags
|
||||
badReq := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s", user.Name, repo.Name, commit.ID.String()).
|
||||
|
||||
@@ -34,6 +34,7 @@ func TestAPICreateHook(t *testing.T) {
|
||||
"url": "http://example.com/",
|
||||
},
|
||||
AuthorizationHeader: "Bearer s3cr3t",
|
||||
Name: " CI notifications ",
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusCreated)
|
||||
|
||||
@@ -41,4 +42,54 @@ func TestAPICreateHook(t *testing.T) {
|
||||
DecodeJSON(t, resp, &apiHook)
|
||||
assert.Equal(t, "http://example.com/", apiHook.Config["url"])
|
||||
assert.Equal(t, "Bearer s3cr3t", apiHook.AuthorizationHeader)
|
||||
assert.Equal(t, "CI notifications", apiHook.Name)
|
||||
|
||||
newName := "Deploy hook"
|
||||
patchReq := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d", owner.Name, repo.Name, apiHook.ID), api.EditHookOption{
|
||||
Name: &newName,
|
||||
}).AddTokenAuth(token)
|
||||
patchResp := MakeRequest(t, patchReq, http.StatusOK)
|
||||
var patched *api.Hook
|
||||
DecodeJSON(t, patchResp, &patched)
|
||||
assert.Equal(t, newName, patched.Name)
|
||||
|
||||
hooksURL := fmt.Sprintf("/api/v1/repos/%s/%s/hooks", owner.Name, repo.Name)
|
||||
|
||||
// Create with Name field omitted: Name should be ""
|
||||
req2 := NewRequestWithJSON(t, "POST", hooksURL, api.CreateHookOption{
|
||||
Type: "gitea",
|
||||
Config: api.CreateHookOptionConfig{
|
||||
"content_type": "json",
|
||||
"url": "http://example.com/",
|
||||
},
|
||||
}).AddTokenAuth(token)
|
||||
resp2 := MakeRequest(t, req2, http.StatusCreated)
|
||||
var created *api.Hook
|
||||
DecodeJSON(t, resp2, &created)
|
||||
assert.Empty(t, created.Name)
|
||||
|
||||
hookURL := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d", owner.Name, repo.Name, created.ID)
|
||||
|
||||
// PATCH with Name set: existing Name must be updated
|
||||
setName := "original"
|
||||
setReq := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{
|
||||
Name: &setName,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, setReq, http.StatusOK)
|
||||
|
||||
// PATCH without Name field: name must remain "original"
|
||||
patchReq2 := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{}).AddTokenAuth(token)
|
||||
patchResp2 := MakeRequest(t, patchReq2, http.StatusOK)
|
||||
var notCleared *api.Hook
|
||||
DecodeJSON(t, patchResp2, ¬Cleared)
|
||||
assert.Equal(t, "original", notCleared.Name)
|
||||
|
||||
// PATCH with Name: "" explicitly: Name should be cleared to ""
|
||||
clearReq := NewRequestWithJSON(t, "PATCH", hookURL, api.EditHookOption{
|
||||
Name: new(""),
|
||||
}).AddTokenAuth(token)
|
||||
clearResp := MakeRequest(t, clearReq, http.StatusOK)
|
||||
var cleared *api.Hook
|
||||
DecodeJSON(t, clearResp, &cleared)
|
||||
assert.Empty(t, cleared.Name)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ func TestRepoLanguages(t *testing.T) {
|
||||
|
||||
// Save new file to master branch
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
|
||||
"_csrf": doc.GetCSRF(),
|
||||
"last_commit": lastCommit,
|
||||
"tree_path": "test.go",
|
||||
"content": "package main",
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPILFSNotStarted(t *testing.T) {
|
||||
@@ -59,12 +60,12 @@ func TestAPILFSMediaType(t *testing.T) {
|
||||
MakeRequest(t, req, http.StatusUnsupportedMediaType)
|
||||
}
|
||||
|
||||
func createLFSTestRepository(t *testing.T, name string) *repo_model.Repository {
|
||||
ctx := NewAPITestContext(t, "user2", "lfs-"+name+"-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
func createLFSTestRepository(t *testing.T, repoName string) *repo_model.Repository {
|
||||
ctx := NewAPITestContext(t, "user2", repoName, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||
t.Run("CreateRepo", doAPICreateRepository(ctx, false))
|
||||
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", "lfs-"+name+"-repo")
|
||||
assert.NoError(t, err)
|
||||
repo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", repoName)
|
||||
require.NoError(t, err)
|
||||
|
||||
return repo
|
||||
}
|
||||
@@ -74,7 +75,7 @@ func TestAPILFSBatch(t *testing.T) {
|
||||
|
||||
setting.LFS.StartServer = true
|
||||
|
||||
repo := createLFSTestRepository(t, "batch")
|
||||
repo := createLFSTestRepository(t, "lfs-batch-repo")
|
||||
|
||||
content := []byte("dummy1")
|
||||
oid := storeObjectInRepo(t, repo.ID, &content)
|
||||
@@ -253,7 +254,7 @@ func TestAPILFSBatch(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
|
||||
repo2 := createLFSTestRepository(t, "batch2")
|
||||
repo2 := createLFSTestRepository(t, "lfs-batch2-repo")
|
||||
content := []byte("dummy0")
|
||||
storeObjectInRepo(t, repo2.ID, &content)
|
||||
|
||||
@@ -316,6 +317,7 @@ func TestAPILFSBatch(t *testing.T) {
|
||||
ul := br.Objects[0].Actions["upload"]
|
||||
assert.NotNil(t, ul)
|
||||
assert.NotEmpty(t, ul.Href)
|
||||
assert.Equal(t, "chunked", ul.Header["Transfer-Encoding"], "git-lfs client needs Transfer-Encoding to do chunked transfer")
|
||||
assert.Contains(t, br.Objects[0].Actions, "verify")
|
||||
vl := br.Objects[0].Actions["verify"]
|
||||
assert.NotNil(t, vl)
|
||||
@@ -329,7 +331,7 @@ func TestAPILFSUpload(t *testing.T) {
|
||||
|
||||
setting.LFS.StartServer = true
|
||||
|
||||
repo := createLFSTestRepository(t, "upload")
|
||||
repo := createLFSTestRepository(t, "lfs-upload-repo")
|
||||
|
||||
content := []byte("dummy3")
|
||||
oid := storeObjectInRepo(t, repo.ID, &content)
|
||||
@@ -433,7 +435,7 @@ func TestAPILFSVerify(t *testing.T) {
|
||||
|
||||
setting.LFS.StartServer = true
|
||||
|
||||
repo := createLFSTestRepository(t, "verify")
|
||||
repo := createLFSTestRepository(t, "lfs-verify-repo")
|
||||
|
||||
content := []byte("dummy3")
|
||||
oid := storeObjectInRepo(t, repo.ID, &content)
|
||||
|
||||
@@ -43,7 +43,6 @@ func TestAPIRepoLicense(t *testing.T) {
|
||||
|
||||
// Save new file to master branch
|
||||
req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
|
||||
"_csrf": doc.GetCSRF(),
|
||||
"last_commit": lastCommit,
|
||||
"tree_path": "LICENSE",
|
||||
"content": testLicenseContent,
|
||||
|
||||
@@ -671,6 +671,16 @@ func TestAPIGenerateRepo(t *testing.T) {
|
||||
|
||||
templateRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 44})
|
||||
|
||||
assertGeneratedRepoIsUsable := func(t *testing.T, ownerName string, repo *api.Repository) {
|
||||
t.Helper()
|
||||
assert.NotEmpty(t, repo.DefaultBranch)
|
||||
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s", ownerName, repo.Name, repo.DefaultBranch).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
branch := DecodeJSON(t, resp, &api.Branch{})
|
||||
assert.Equal(t, repo.DefaultBranch, branch.Name)
|
||||
}
|
||||
|
||||
// user
|
||||
repo := new(api.Repository)
|
||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
|
||||
@@ -684,6 +694,7 @@ func TestAPIGenerateRepo(t *testing.T) {
|
||||
DecodeJSON(t, resp, repo)
|
||||
|
||||
assert.Equal(t, "new-repo", repo.Name)
|
||||
assertGeneratedRepoIsUsable(t, user.Name, repo)
|
||||
|
||||
// org
|
||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/generate", templateRepo.OwnerName, templateRepo.Name), &api.GenerateRepoOption{
|
||||
@@ -697,6 +708,7 @@ func TestAPIGenerateRepo(t *testing.T) {
|
||||
DecodeJSON(t, resp, repo)
|
||||
|
||||
assert.Equal(t, "new-repo", repo.Name)
|
||||
assertGeneratedRepoIsUsable(t, "org3", repo)
|
||||
}
|
||||
|
||||
func TestAPIRepoGetReviewers(t *testing.T) {
|
||||
|
||||
@@ -502,11 +502,12 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model
|
||||
}
|
||||
unauthorizedLevel := auth_model.Write
|
||||
if categoryIsRequired {
|
||||
if minRequiredLevel == auth_model.Read {
|
||||
switch minRequiredLevel {
|
||||
case auth_model.Read:
|
||||
unauthorizedLevel = auth_model.NoAccess
|
||||
} else if minRequiredLevel == auth_model.Write {
|
||||
case auth_model.Write:
|
||||
unauthorizedLevel = auth_model.Read
|
||||
} else {
|
||||
default:
|
||||
assert.FailNow(t, "Invalid test case", "Unknown access token scope level: %v", minRequiredLevel)
|
||||
}
|
||||
}
|
||||
@@ -514,10 +515,10 @@ func runTestCase(t *testing.T, testCase *requiredScopeTestCase, user *user_model
|
||||
if unauthorizedLevel == auth_model.NoAccess {
|
||||
continue
|
||||
}
|
||||
cateogoryUnauthorizedScopes := auth_model.GetRequiredScopes(
|
||||
categoryUnauthorizedScopes := auth_model.GetRequiredScopes(
|
||||
unauthorizedLevel,
|
||||
category)
|
||||
unauthorizedScopes = append(unauthorizedScopes, cateogoryUnauthorizedScopes...)
|
||||
unauthorizedScopes = append(unauthorizedScopes, categoryUnauthorizedScopes...)
|
||||
}
|
||||
|
||||
accessToken := createAPIAccessTokenWithoutCleanUp(t, "test-token", user, unauthorizedScopes)
|
||||
|
||||
@@ -15,6 +15,21 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPermissionsAPI(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
t.Run("TokenNeeded", testTokenNeeded)
|
||||
t.Run("WithOwnerUser", testWithOwnerUser)
|
||||
t.Run("CanWriteUser", testCanWriteUser)
|
||||
t.Run("AdminUser", testAdminUser)
|
||||
t.Run("AdminCanNotCreateRepo", testAdminCanNotCreateRepo)
|
||||
t.Run("CanReadUser", testCanReadUser)
|
||||
t.Run("UnknownUser", testUnknownUser)
|
||||
t.Run("UnknownOrganization", testUnknownOrganization)
|
||||
t.Run("HiddenMemberPermissionsForbidden", testHiddenMemberPermissionsForbidden)
|
||||
t.Run("PrivateOrgPermissionsNotFound", testPrivateOrgPermissionsNotFound)
|
||||
}
|
||||
|
||||
type apiUserOrgPermTestCase struct {
|
||||
LoginUser string
|
||||
User string
|
||||
@@ -22,16 +37,12 @@ type apiUserOrgPermTestCase struct {
|
||||
ExpectedOrganizationPermissions api.OrganizationPermissions
|
||||
}
|
||||
|
||||
func TestTokenNeeded(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testTokenNeeded(t *testing.T) {
|
||||
req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/org6/permissions")
|
||||
MakeRequest(t, req, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func sampleTest(t *testing.T, auoptc apiUserOrgPermTestCase) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, auoptc.LoginUser)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser)
|
||||
|
||||
@@ -48,7 +59,7 @@ func sampleTest(t *testing.T, auoptc apiUserOrgPermTestCase) {
|
||||
assert.Equal(t, auoptc.ExpectedOrganizationPermissions.CanCreateRepository, apiOP.CanCreateRepository)
|
||||
}
|
||||
|
||||
func TestWithOwnerUser(t *testing.T) {
|
||||
func testWithOwnerUser(t *testing.T) {
|
||||
sampleTest(t, apiUserOrgPermTestCase{
|
||||
LoginUser: "user2",
|
||||
User: "user2",
|
||||
@@ -63,7 +74,7 @@ func TestWithOwnerUser(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCanWriteUser(t *testing.T) {
|
||||
func testCanWriteUser(t *testing.T) {
|
||||
sampleTest(t, apiUserOrgPermTestCase{
|
||||
LoginUser: "user4",
|
||||
User: "user4",
|
||||
@@ -78,7 +89,7 @@ func TestCanWriteUser(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminUser(t *testing.T) {
|
||||
func testAdminUser(t *testing.T) {
|
||||
sampleTest(t, apiUserOrgPermTestCase{
|
||||
LoginUser: "user1",
|
||||
User: "user28",
|
||||
@@ -93,7 +104,7 @@ func TestAdminUser(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdminCanNotCreateRepo(t *testing.T) {
|
||||
func testAdminCanNotCreateRepo(t *testing.T) {
|
||||
sampleTest(t, apiUserOrgPermTestCase{
|
||||
LoginUser: "user1",
|
||||
User: "user28",
|
||||
@@ -108,7 +119,7 @@ func TestAdminCanNotCreateRepo(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCanReadUser(t *testing.T) {
|
||||
func testCanReadUser(t *testing.T) {
|
||||
sampleTest(t, apiUserOrgPermTestCase{
|
||||
LoginUser: "user1",
|
||||
User: "user24",
|
||||
@@ -123,31 +134,62 @@ func TestCanReadUser(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnknowUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUnknownUser(t *testing.T) {
|
||||
session := loginUser(t, "user1")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/users/unknow/orgs/org25/permissions").
|
||||
req := NewRequest(t, "GET", "/api/v1/users/unknown/orgs/org25/permissions").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
var apiError api.APIError
|
||||
DecodeJSON(t, resp, &apiError)
|
||||
assert.Equal(t, "user redirect does not exist [name: unknow]", apiError.Message)
|
||||
assert.Equal(t, "user redirect does not exist [name: unknown]", apiError.Message)
|
||||
}
|
||||
|
||||
func TestUnknowOrganization(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUnknownOrganization(t *testing.T) {
|
||||
session := loginUser(t, "user1")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/unknow/permissions").
|
||||
req := NewRequest(t, "GET", "/api/v1/users/user1/orgs/unknown/permissions").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusNotFound)
|
||||
var apiError api.APIError
|
||||
DecodeJSON(t, resp, &apiError)
|
||||
assert.Equal(t, "GetUserByName", apiError.Message)
|
||||
}
|
||||
|
||||
func testHiddenMemberPermissionsForbidden(t *testing.T) {
|
||||
session := loginUser(t, "user8")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/users/user5/orgs/privated_org/permissions").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
adminSession := loginUser(t, "user1")
|
||||
adminToken := getTokenForLoggedInUser(t, adminSession, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
|
||||
|
||||
adminReq := NewRequest(t, "GET", "/api/v1/users/user5/orgs/privated_org/permissions").
|
||||
AddTokenAuth(adminToken)
|
||||
resp := MakeRequest(t, adminReq, http.StatusOK)
|
||||
|
||||
var apiOP api.OrganizationPermissions
|
||||
DecodeJSON(t, resp, &apiOP)
|
||||
assert.Equal(t, api.OrganizationPermissions{
|
||||
IsOwner: false,
|
||||
IsAdmin: false,
|
||||
CanWrite: true,
|
||||
CanRead: true,
|
||||
CanCreateRepository: true,
|
||||
}, apiOP)
|
||||
}
|
||||
|
||||
func testPrivateOrgPermissionsNotFound(t *testing.T) {
|
||||
session := loginUser(t, "user8")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadOrganization)
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/users/user5/orgs/privated_org/permissions").
|
||||
AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
|
||||
@@ -155,3 +155,44 @@ func TestMyOrgs(t *testing.T) {
|
||||
},
|
||||
}, orgs)
|
||||
}
|
||||
|
||||
func TestMyOrgsPublicOnly(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
normalUsername := "user2"
|
||||
token := getUserToken(t, normalUsername, auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopePublicOnly)
|
||||
req := NewRequest(t, "GET", "/api/v1/user/orgs").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var orgs []*api.Organization
|
||||
DecodeJSON(t, resp, &orgs)
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
|
||||
org17 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org17"})
|
||||
|
||||
assert.Equal(t, []*api.Organization{
|
||||
{
|
||||
ID: 17,
|
||||
Name: org17.Name,
|
||||
UserName: org17.Name,
|
||||
FullName: org17.FullName,
|
||||
Email: org17.Email,
|
||||
AvatarURL: org17.AvatarLink(t.Context()),
|
||||
Description: "",
|
||||
Website: "",
|
||||
Location: "",
|
||||
Visibility: "public",
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Name: org3.Name,
|
||||
UserName: org3.Name,
|
||||
FullName: org3.FullName,
|
||||
Email: org3.Email,
|
||||
AvatarURL: org3.AvatarLink(t.Context()),
|
||||
Description: "",
|
||||
Website: "",
|
||||
Location: "",
|
||||
Visibility: "public",
|
||||
},
|
||||
}, orgs)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIPublicOnlySelfUserRoutes(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user31"})
|
||||
require.True(t, privateUser.Visibility.IsPrivate())
|
||||
|
||||
privateSession := loginUser(t, privateUser.Name)
|
||||
privateReadUserToken := getTokenForLoggedInUser(t, privateSession,
|
||||
auth_model.AccessTokenScopePublicOnly,
|
||||
auth_model.AccessTokenScopeReadUser,
|
||||
)
|
||||
privateWriteUserToken := getTokenForLoggedInUser(t, privateSession,
|
||||
auth_model.AccessTokenScopePublicOnly,
|
||||
auth_model.AccessTokenScopeWriteUser,
|
||||
)
|
||||
|
||||
t.Run("PrivateProfileForbidden", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user31").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("PrivateSensitiveSelfRoutesForbidden", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/settings").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
hideEmail := true
|
||||
settingsReq := NewRequestWithJSON(t, "PATCH", "/api/v1/user/settings", &api.UserSettingsOptions{
|
||||
HideEmail: &hideEmail,
|
||||
}).AddTokenAuth(privateWriteUserToken)
|
||||
MakeRequest(t, settingsReq, http.StatusForbidden)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/emails").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
emailReq := NewRequestWithJSON(t, "POST", "/api/v1/user/emails", &api.CreateEmailOption{
|
||||
Emails: []string{"user31-public-only@example.com"},
|
||||
}).AddTokenAuth(privateWriteUserToken)
|
||||
MakeRequest(t, emailReq, http.StatusForbidden)
|
||||
|
||||
keyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/keys", api.CreateKeyOption{
|
||||
Title: "public-only-private-key",
|
||||
Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment",
|
||||
}).AddTokenAuth(privateWriteUserToken)
|
||||
MakeRequest(t, keyReq, http.StatusForbidden)
|
||||
|
||||
oauthReq := NewRequestWithJSON(t, "POST", "/api/v1/user/applications/oauth2", &api.CreateOAuth2ApplicationOptions{
|
||||
Name: "public-only-private-oauth-app",
|
||||
RedirectURIs: []string{"https://example.com/callback"},
|
||||
ConfidentialClient: true,
|
||||
}).AddTokenAuth(privateWriteUserToken)
|
||||
MakeRequest(t, oauthReq, http.StatusForbidden)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_keys").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
gpgKeyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_keys", &api.CreateGPGKeyOption{
|
||||
ArmoredKey: "-----BEGIN PGP PUBLIC KEY BLOCK-----\ncomment\n-----END PGP PUBLIC KEY BLOCK-----",
|
||||
}).AddTokenAuth(privateWriteUserToken)
|
||||
MakeRequest(t, gpgKeyReq, http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/gpg_key_token").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
gpgVerifyReq := NewRequestWithJSON(t, "POST", "/api/v1/user/gpg_key_verify", &api.VerifyGPGKeyOption{
|
||||
KeyID: "deadbeef",
|
||||
Signature: "invalid-signature",
|
||||
}).AddTokenAuth(privateWriteUserToken)
|
||||
MakeRequest(t, gpgVerifyReq, http.StatusForbidden)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/actions/variables").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/actions/secrets/PRIVATE_SECRET").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
||||
variableReq := NewRequestWithJSON(t, "POST", "/api/v1/user/actions/variables/PRIVATE_VAR", api.CreateVariableOption{
|
||||
Value: "private-value",
|
||||
Description: "must stay private",
|
||||
}).AddTokenAuth(privateWriteUserToken)
|
||||
MakeRequest(t, variableReq, http.StatusForbidden)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "POST", "/api/v1/user/actions/runners/registration-token").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/hooks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
hookReq := NewRequestWithJSON(t, "POST", "/api/v1/user/hooks", api.CreateHookOption{
|
||||
Type: "gitea",
|
||||
Config: api.CreateHookOptionConfig{
|
||||
"content_type": "json",
|
||||
"url": "http://example.com/",
|
||||
},
|
||||
Name: "public-only-private-hook",
|
||||
}).AddTokenAuth(privateWriteUserToken)
|
||||
MakeRequest(t, hookReq, http.StatusForbidden)
|
||||
|
||||
avatarReq := NewRequestWithJSON(t, "POST", "/api/v1/user/avatar", &api.UpdateUserAvatarOption{
|
||||
Image: "aGVsbG8=",
|
||||
}).AddTokenAuth(privateWriteUserToken)
|
||||
MakeRequest(t, avatarReq, http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/avatar").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/times").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/stopwatches").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/subscriptions").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/teams").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/blocks").AddTokenAuth(privateReadUserToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/blocks/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "DELETE", "/api/v1/user/following/user2").AddTokenAuth(privateWriteUserToken), http.StatusForbidden)
|
||||
})
|
||||
|
||||
t.Run("PublicRepoRoutesFilterAndRejectMutations", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
publicSession := loginUser(t, "user2")
|
||||
fullWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
|
||||
auth_model.AccessTokenScopeWriteUser,
|
||||
auth_model.AccessTokenScopeWriteRepository,
|
||||
)
|
||||
publicOnlyReadRepoToken := getTokenForLoggedInUser(t, publicSession,
|
||||
auth_model.AccessTokenScopePublicOnly,
|
||||
auth_model.AccessTokenScopeReadUser,
|
||||
auth_model.AccessTokenScopeReadRepository,
|
||||
)
|
||||
publicOnlyWriteRepoToken := getTokenForLoggedInUser(t, publicSession,
|
||||
auth_model.AccessTokenScopePublicOnly,
|
||||
auth_model.AccessTokenScopeWriteUser,
|
||||
auth_model.AccessTokenScopeWriteRepository,
|
||||
)
|
||||
|
||||
publicRepoName := "public-only-visible-self-repo"
|
||||
privateRepoName := "public-only-hidden-self-repo"
|
||||
|
||||
resp := MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
||||
Name: publicRepoName,
|
||||
Private: false,
|
||||
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
|
||||
publicRepo := DecodeJSON(t, resp, &api.Repository{})
|
||||
require.Equal(t, "user2/"+publicRepoName, publicRepo.FullName)
|
||||
|
||||
resp = MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
||||
Name: privateRepoName,
|
||||
Private: true,
|
||||
}).AddTokenAuth(fullWriteRepoToken), http.StatusCreated)
|
||||
privateRepo := DecodeJSON(t, resp, &api.Repository{})
|
||||
require.Equal(t, "user2/"+privateRepoName, privateRepo.FullName)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", "/api/v1/repos/user2/"+privateRepoName).AddTokenAuth(publicOnlyReadRepoToken), http.StatusNotFound)
|
||||
|
||||
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/repos").AddTokenAuth(publicOnlyReadRepoToken), http.StatusOK)
|
||||
repos := DecodeJSON(t, resp, []api.Repository{})
|
||||
|
||||
foundPublicRepo := false
|
||||
for _, repo := range repos {
|
||||
require.NotEqual(t, privateRepo.FullName, repo.FullName)
|
||||
if repo.FullName == publicRepo.FullName {
|
||||
foundPublicRepo = true
|
||||
}
|
||||
}
|
||||
require.True(t, foundPublicRepo)
|
||||
|
||||
MakeRequest(t, NewRequestWithJSON(t, "POST", "/api/v1/user/repos", &api.CreateRepoOption{
|
||||
Name: "public-only-rejected-self-repo",
|
||||
Private: false,
|
||||
}).AddTokenAuth(publicOnlyWriteRepoToken), http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPIStar(t *testing.T) {
|
||||
@@ -155,3 +156,24 @@ func TestAPIStarDisabled(t *testing.T) {
|
||||
MakeRequest(t, req, http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIStarPublicOnly(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
|
||||
req := NewRequest(t, "GET", "/api/v1/user/starred").
|
||||
AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
repos := DecodeJSON(t, resp, []api.Repository{})
|
||||
if assert.Len(t, repos, 1) {
|
||||
assert.Equal(t, "user5/repo4", repos[0].FullName)
|
||||
}
|
||||
|
||||
req = NewRequest(t, "GET", "/api/v1/users/user2/starred").
|
||||
AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
repos = DecodeJSON(t, resp, []api.Repository{})
|
||||
require.Len(t, repos, 1)
|
||||
assert.Equal(t, "user5/repo4", repos[0].FullName)
|
||||
}
|
||||
|
||||
@@ -94,3 +94,28 @@ func TestAPIWatch(t *testing.T) {
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIWatchPublicOnly(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
writeRepoToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeReadUser)
|
||||
publicOnlyToken := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopePublicOnly, auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadRepository)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/repos/user2/repo1/subscription").AddTokenAuth(writeRepoToken), http.StatusOK)
|
||||
MakeRequest(t, NewRequest(t, "PUT", "/api/v1/repos/user2/repo2/subscription").AddTokenAuth(writeRepoToken), http.StatusOK)
|
||||
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", "/api/v1/user/subscriptions").AddTokenAuth(publicOnlyToken), http.StatusOK)
|
||||
repos := DecodeJSON(t, resp, []api.Repository{})
|
||||
for _, r := range repos {
|
||||
assert.False(t, r.Private, "private repo %s leaked via /user/subscriptions", r.FullName)
|
||||
}
|
||||
assert.NotContains(t, repoNames(repos), "user2/repo2")
|
||||
|
||||
resp = MakeRequest(t, NewRequest(t, "GET", "/api/v1/users/user1/subscriptions").AddTokenAuth(publicOnlyToken), http.StatusOK)
|
||||
repos = DecodeJSON(t, resp, []api.Repository{})
|
||||
for _, r := range repos {
|
||||
assert.False(t, r.Private, "private repo %s leaked via /users/{username}/subscriptions", r.FullName)
|
||||
}
|
||||
assert.NotContains(t, repoNames(repos), "user2/repo2")
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
@@ -26,6 +27,15 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type attachmentScopeCase struct {
|
||||
name string
|
||||
url string
|
||||
readIssueStatus int
|
||||
readRepoStatus int
|
||||
publicOnlyIssueStatus int
|
||||
publicOnlyRepoStatus int
|
||||
}
|
||||
|
||||
func testGeneratePngBytes() []byte {
|
||||
myImage := image.NewRGBA(image.Rect(0, 0, 32, 32))
|
||||
var buff bytes.Buffer
|
||||
@@ -33,15 +43,15 @@ func testGeneratePngBytes() []byte {
|
||||
return buff.Bytes()
|
||||
}
|
||||
|
||||
func testCreateIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL, filename string, content []byte, expectedStatus int) string {
|
||||
return testCreateAttachment(t, session, csrf, repoURL, "issues", filename, content, expectedStatus)
|
||||
func testCreateIssueAttachment(t *testing.T, session *TestSession, repoURL, filename string, content []byte, expectedStatus int) string {
|
||||
return testCreateAttachment(t, session, repoURL, "issues", filename, content, expectedStatus)
|
||||
}
|
||||
|
||||
func testCreateReleaseAttachment(t *testing.T, session *TestSession, csrf, repoURL, filename string, content []byte, expectedStatus int) string {
|
||||
return testCreateAttachment(t, session, csrf, repoURL, "releases", filename, content, expectedStatus)
|
||||
func testCreateReleaseAttachment(t *testing.T, session *TestSession, repoURL, filename string, content []byte, expectedStatus int) string {
|
||||
return testCreateAttachment(t, session, repoURL, "releases", filename, content, expectedStatus)
|
||||
}
|
||||
|
||||
func testCreateAttachment(t *testing.T, session *TestSession, csrf, repoURL, issueOrRelease, filename string, content []byte, expectedStatus int) string {
|
||||
func testCreateAttachment(t *testing.T, session *TestSession, repoURL, issueOrRelease, filename string, content []byte, expectedStatus int) string {
|
||||
body := &bytes.Buffer{}
|
||||
|
||||
// Setup multi-part
|
||||
@@ -54,7 +64,6 @@ func testCreateAttachment(t *testing.T, session *TestSession, csrf, repoURL, iss
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST", repoURL+"/"+issueOrRelease+"/attachments", body)
|
||||
req.Header.Add("X-Csrf-Token", csrf)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
resp := session.MakeRequest(t, req, expectedStatus)
|
||||
|
||||
@@ -66,15 +75,13 @@ func testCreateAttachment(t *testing.T, session *TestSession, csrf, repoURL, iss
|
||||
return obj["uuid"]
|
||||
}
|
||||
|
||||
func testDeleteIssueAttachment(t *testing.T, session *TestSession, csrf, repoURL, uuid string, expectedStatus int) {
|
||||
func testDeleteIssueAttachment(t *testing.T, session *TestSession, repoURL, uuid string, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", repoURL+"/issues/attachments/remove", map[string]string{"file": uuid})
|
||||
req.Header.Add("X-Csrf-Token", csrf)
|
||||
session.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
func testDeleteReleaseAttachment(t *testing.T, session *TestSession, csrf, repoURL, uuid string, expectedStatus int) {
|
||||
func testDeleteReleaseAttachment(t *testing.T, session *TestSession, repoURL, uuid string, expectedStatus int) {
|
||||
req := NewRequestWithValues(t, "POST", repoURL+"/releases/attachments/remove", map[string]string{"file": uuid})
|
||||
req.Header.Add("X-Csrf-Token", csrf)
|
||||
session.MakeRequest(t, req, expectedStatus)
|
||||
}
|
||||
|
||||
@@ -100,20 +107,20 @@ func testUploadAttachmentDeleteTemp(t *testing.T) {
|
||||
defer web.RouteMock(route_web.RouterMockPointBeforeWebRoutes, func(resp http.ResponseWriter, req *http.Request) {
|
||||
tmpFileCountDuringUpload = countTmpFile()
|
||||
})()
|
||||
_ = testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK)
|
||||
_ = testCreateIssueAttachment(t, session, "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusOK)
|
||||
assert.Equal(t, 1, tmpFileCountDuringUpload, "the temp file should exist when uploaded size exceeds the parse form's max memory")
|
||||
assert.Equal(t, 0, countTmpFile(), "the temp file should be deleted after upload")
|
||||
}
|
||||
|
||||
func testCreateAnonymousAttachment(t *testing.T) {
|
||||
session := emptyTestSession(t)
|
||||
testCreateIssueAttachment(t, session, GetAnonymousCSRFToken(t, session), "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
|
||||
testCreateIssueAttachment(t, session, "user2/repo1", "image.png", testGeneratePngBytes(), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func testCreateUser2IssueAttachment(t *testing.T) {
|
||||
const repoURL = "user2/repo1"
|
||||
session := loginUser(t, "user2")
|
||||
uuid := testCreateIssueAttachment(t, session, GetUserCSRFToken(t, session), repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
|
||||
uuid := testCreateIssueAttachment(t, session, repoURL, "image.png", testGeneratePngBytes(), http.StatusOK)
|
||||
|
||||
req := NewRequest(t, "GET", repoURL+"/issues/new")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
@@ -123,7 +130,6 @@ func testCreateUser2IssueAttachment(t *testing.T) {
|
||||
assert.True(t, exists, "The template has changed")
|
||||
|
||||
postData := map[string]string{
|
||||
"_csrf": htmlDoc.GetCSRF(),
|
||||
"title": "New Issue With Attachment",
|
||||
"content": "some content",
|
||||
"files": uuid,
|
||||
@@ -187,24 +193,118 @@ func testDeleteAttachmentPermissions(t *testing.T) {
|
||||
ownerSession := loginUser(t, "user2")
|
||||
readonlySession := loginUser(t, "user5")
|
||||
|
||||
ownerCSRF := GetUserCSRFToken(t, ownerSession)
|
||||
readonlyCSRF := GetUserCSRFToken(t, readonlySession)
|
||||
issueFromOwner := testCreateIssueAttachment(t, ownerSession, repoURL, "owner-issue.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteIssueAttachment(t, readonlySession, repoURL, issueFromOwner, http.StatusForbidden)
|
||||
|
||||
issueFromOwner := testCreateIssueAttachment(t, ownerSession, ownerCSRF, repoURL, "owner-issue.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteIssueAttachment(t, readonlySession, readonlyCSRF, repoURL, issueFromOwner, http.StatusForbidden)
|
||||
issueFromReader := testCreateIssueAttachment(t, readonlySession, repoURL, "reader-issue.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteIssueAttachment(t, ownerSession, repoURL, issueFromReader, http.StatusOK)
|
||||
|
||||
issueFromReader := testCreateIssueAttachment(t, readonlySession, readonlyCSRF, repoURL, "reader-issue.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, repoURL, issueFromReader, http.StatusOK)
|
||||
testCreateReleaseAttachment(t, readonlySession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusNotFound)
|
||||
|
||||
testCreateReleaseAttachment(t, readonlySession, readonlyCSRF, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusNotFound)
|
||||
crossRepoUUID := testCreateIssueAttachment(t, ownerSession, repoURL, "cross-repo.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteIssueAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
|
||||
testDeleteIssueAttachment(t, ownerSession, repoURL, crossRepoUUID, http.StatusOK)
|
||||
|
||||
crossRepoUUID := testCreateIssueAttachment(t, ownerSession, ownerCSRF, repoURL, "cross-repo.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
|
||||
testDeleteIssueAttachment(t, ownerSession, ownerCSRF, repoURL, crossRepoUUID, http.StatusOK)
|
||||
|
||||
releaseUUID := testCreateReleaseAttachment(t, ownerSession, ownerCSRF, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteReleaseAttachment(t, ownerSession, ownerCSRF, repoURL, releaseUUID, http.StatusOK)
|
||||
releaseUUID := testCreateReleaseAttachment(t, ownerSession, repoURL, "reader-release.png", testGeneratePngBytes(), http.StatusOK)
|
||||
testDeleteReleaseAttachment(t, ownerSession, repoURL, releaseUUID, http.StatusOK)
|
||||
|
||||
// test deleting release attachment from another repo
|
||||
testDeleteReleaseAttachment(t, ownerSession, ownerCSRF, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
|
||||
testDeleteReleaseAttachment(t, ownerSession, "user2/repo2", crossRepoUUID, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func TestAttachmentTokenScopes(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
for _, uuid := range []string{
|
||||
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
|
||||
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
|
||||
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
|
||||
"a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
|
||||
} {
|
||||
_, err := storage.Attachments.Save(repo_model.AttachmentRelativePath(uuid), strings.NewReader("hello world"), -1)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
readIssueToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadIssue)
|
||||
readRepoToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository)
|
||||
miscToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadMisc)
|
||||
publicOnlyIssueToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadIssue, auth_model.AccessTokenScopePublicOnly)
|
||||
publicOnlyRepoToken := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopePublicOnly)
|
||||
|
||||
cases := []attachmentScopeCase{
|
||||
{
|
||||
name: "GlobalPublicIssueAttachment",
|
||||
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
|
||||
readIssueStatus: http.StatusOK,
|
||||
readRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusOK,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "RepoPublicIssueAttachment",
|
||||
url: "/user2/repo1/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11",
|
||||
readIssueStatus: http.StatusOK,
|
||||
readRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusOK,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "GlobalPrivateIssueAttachment",
|
||||
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
|
||||
readIssueStatus: http.StatusOK,
|
||||
readRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "RepoPrivateIssueAttachment",
|
||||
url: "/user2/repo2/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12",
|
||||
readIssueStatus: http.StatusOK,
|
||||
readRepoStatus: http.StatusForbidden,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "GlobalPublicReleaseAttachment",
|
||||
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
|
||||
readIssueStatus: http.StatusForbidden,
|
||||
readRepoStatus: http.StatusOK,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "RepoPublicReleaseAttachment",
|
||||
url: "/user2/repo1/releases/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a19",
|
||||
readIssueStatus: http.StatusForbidden,
|
||||
readRepoStatus: http.StatusOK,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "GlobalPrivateReleaseAttachment",
|
||||
url: "/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
|
||||
readIssueStatus: http.StatusForbidden,
|
||||
readRepoStatus: http.StatusOK,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "RepoPrivateReleaseAttachment",
|
||||
url: "/user2/repo2/releases/attachments/a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22",
|
||||
readIssueStatus: http.StatusForbidden,
|
||||
readRepoStatus: http.StatusOK,
|
||||
publicOnlyIssueStatus: http.StatusForbidden,
|
||||
publicOnlyRepoStatus: http.StatusForbidden,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(miscToken), http.StatusForbidden)
|
||||
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(readIssueToken), tc.readIssueStatus)
|
||||
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(readRepoToken), tc.readRepoStatus)
|
||||
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(publicOnlyIssueToken), tc.publicOnlyIssueStatus)
|
||||
MakeRequest(t, NewRequest(t, "GET", tc.url).AddTokenAuth(publicOnlyRepoToken), tc.publicOnlyRepoStatus)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user